fix(ivy): ngcc should compile entry-points in the correct order (#25862)
The compiler should process all an entry-points dependencies before processing that entry-point. PR Close #25862
This commit is contained in:
parent
976389836e
commit
9b1bb370a3
|
@ -0,0 +1 @@
|
|||
yarn.lock
|
|
@ -4,10 +4,17 @@ set -e -x
|
|||
|
||||
PATH=$PATH:$(npm bin)
|
||||
|
||||
ivy-ngcc fesm2015,esm2015
|
||||
ivy-ngcc --help
|
||||
|
||||
# node --inspect-brk $(npm bin)/ivy-ngcc -f esm2015
|
||||
ivy-ngcc
|
||||
|
||||
# Did it add the appropriate build markers?
|
||||
|
||||
# - fesm2015
|
||||
ls node_modules/@angular/common | grep __modified_by_ngcc_for_fesm2015
|
||||
if [[ $? != 0 ]]; then exit 1; fi
|
||||
# - esm2015
|
||||
ls node_modules/@angular/common | grep __modified_by_ngcc_for_esm2015
|
||||
if [[ $? != 0 ]]; then exit 1; fi
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"@types/shelljs": "^0.7.8",
|
||||
"@types/source-map": "^0.5.1",
|
||||
"@types/systemjs": "0.19.32",
|
||||
"@types/yargs": "^11.1.1",
|
||||
"@webcomponents/custom-elements": "^1.0.4",
|
||||
"angular": "npm:angular@1.7",
|
||||
"angular-1.5": "npm:angular@1.5",
|
||||
|
@ -76,6 +77,7 @@
|
|||
"conventional-changelog": "1.1.0",
|
||||
"convert-source-map": "^1.5.1",
|
||||
"cors": "2.8.4",
|
||||
"dependency-graph": "^0.7.2",
|
||||
"diff": "^3.5.0",
|
||||
"domino": "2.0.1",
|
||||
"entities": "1.1.1",
|
||||
|
|
|
@ -12,10 +12,14 @@
|
|||
"dependencies": {
|
||||
"reflect-metadata": "^0.1.2",
|
||||
"minimist": "^1.2.0",
|
||||
"canonical-path": "0.0.2",
|
||||
"chokidar": "^1.4.2",
|
||||
"convert-source-map": "^1.5.1",
|
||||
"dependency-graph": "^0.7.2",
|
||||
"magic-string": "^0.25.0",
|
||||
"source-map": "^0.6.1"
|
||||
"shelljs": "^0.8.1",
|
||||
"source-map": "^0.6.1",
|
||||
"yargs": "9.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "0.0.0-PLACEHOLDER",
|
||||
|
|
|
@ -7,85 +7,53 @@
|
|||
*/
|
||||
import * as path from 'canonical-path';
|
||||
import {existsSync, lstatSync, readFileSync, readdirSync} from 'fs';
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {PackageTransformer} from './transform/package_transformer';
|
||||
import {DependencyHost} from './packages/dependency_host';
|
||||
import {DependencyResolver} from './packages/dependency_resolver';
|
||||
import {EntryPointFormat} from './packages/entry_point';
|
||||
import {EntryPointFinder} from './packages/entry_point_finder';
|
||||
import {Transformer} from './packages/transformer';
|
||||
|
||||
export function mainNgcc(args: string[]): number {
|
||||
const formats = args[0] ? args[0].split(',') : ['fesm2015', 'esm2015', 'fesm5', 'esm5'];
|
||||
const packagePaths = args[1] ? [path.resolve(args[1])] : findPackagesToCompile();
|
||||
const targetPath = args[2] ? args[2] : 'node_modules';
|
||||
const options =
|
||||
yargs
|
||||
.option('s', {
|
||||
alias: 'source',
|
||||
describe: 'A path to the root folder to compile.',
|
||||
default: './node_modules'
|
||||
})
|
||||
.option('f', {
|
||||
alias: 'formats',
|
||||
array: true,
|
||||
describe: 'An array of formats to compile.',
|
||||
default: ['fesm2015', 'esm2015', 'fesm5', 'esm5']
|
||||
})
|
||||
.option('t', {
|
||||
alias: 'target',
|
||||
describe: 'A path to a root folder where the compiled files will be written.',
|
||||
defaultDescription: 'The `source` folder.'
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
|
||||
const transformer = new PackageTransformer();
|
||||
packagePaths.forEach(packagePath => {
|
||||
formats.forEach(format => {
|
||||
// TODO: remove before flight
|
||||
console.warn(`Compiling ${packagePath} : ${format}`);
|
||||
transformer.transform(packagePath, format, targetPath);
|
||||
});
|
||||
});
|
||||
const sourcePath: string = path.resolve(options['s']);
|
||||
const formats: EntryPointFormat[] = options['f'];
|
||||
const targetPath: string = options['t'] || sourcePath;
|
||||
|
||||
const transformer = new Transformer(sourcePath, targetPath);
|
||||
const host = new DependencyHost();
|
||||
const resolver = new DependencyResolver(host);
|
||||
const finder = new EntryPointFinder(resolver);
|
||||
|
||||
try {
|
||||
const {entryPoints} = finder.findEntryPoints(sourcePath);
|
||||
entryPoints.forEach(
|
||||
entryPoint => formats.forEach(format => transformer.transform(entryPoint, format)));
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO - consider nested node_modules
|
||||
|
||||
/**
|
||||
* Check whether the given folder needs to be included in the ngcc compilation.
|
||||
* We do not care about folders that are:
|
||||
*
|
||||
* - symlinks
|
||||
* - node_modules
|
||||
* - do not contain a package.json
|
||||
* - do not have a typings property in package.json
|
||||
* - do not have an appropriate metadata.json file
|
||||
*
|
||||
* @param folderPath The absolute path to the folder.
|
||||
*/
|
||||
function hasMetadataFile(folderPath: string): boolean {
|
||||
const folderName = path.basename(folderPath);
|
||||
if (folderName === 'node_modules' || lstatSync(folderPath).isSymbolicLink()) {
|
||||
return false;
|
||||
}
|
||||
const packageJsonPath = path.join(folderPath, 'package.json');
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
return false;
|
||||
}
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
if (!packageJson.typings) {
|
||||
return false;
|
||||
}
|
||||
// TODO: avoid if packageJson contains built marker
|
||||
const metadataPath =
|
||||
path.join(folderPath, packageJson.typings.replace(/\.d\.ts$/, '.metadata.json'));
|
||||
return existsSync(metadataPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for packages that need to be compiled.
|
||||
* The function will recurse into folders that start with `@...`, e.g. `@angular/...`.
|
||||
* Without an argument it starts at `node_modules`.
|
||||
*/
|
||||
function findPackagesToCompile(folder: string = 'node_modules'): string[] {
|
||||
const fullPath = path.resolve(folder);
|
||||
const packagesToCompile: string[] = [];
|
||||
readdirSync(fullPath)
|
||||
.filter(p => !p.startsWith('.'))
|
||||
.filter(p => lstatSync(path.join(fullPath, p)).isDirectory())
|
||||
.forEach(p => {
|
||||
const packagePath = path.join(fullPath, p);
|
||||
if (p.startsWith('@')) {
|
||||
packagesToCompile.push(...findPackagesToCompile(packagePath));
|
||||
} else {
|
||||
packagesToCompile.push(packagePath);
|
||||
}
|
||||
});
|
||||
|
||||
return packagesToCompile.filter(path => recursiveDirTest(path, hasMetadataFile));
|
||||
}
|
||||
|
||||
function recursiveDirTest(dir: string, test: (dir: string) => boolean): boolean {
|
||||
return test(dir) || readdirSync(dir).some(segment => {
|
||||
const fullPath = path.join(dir, segment);
|
||||
return lstatSync(fullPath).isDirectory() && recursiveDirTest(fullPath, test);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {resolve} from 'canonical-path';
|
||||
import {existsSync, readFileSync, writeFileSync} from 'fs';
|
||||
import {EntryPoint, EntryPointFormat} from './entry_point';
|
||||
|
||||
export const NGCC_VERSION = '0.0.0-PLACEHOLDER';
|
||||
|
||||
function getMarkerPath(entryPointPath: string, format: EntryPointFormat) {
|
||||
return resolve(entryPointPath, `__modified_by_ngcc_for_${format}__`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there is a build marker for the given entry point and format.
|
||||
* @param entryPoint the entry point to check for a marker.
|
||||
* @param format the format for which we are checking for a marker.
|
||||
*/
|
||||
export function checkMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat): boolean {
|
||||
const markerPath = getMarkerPath(entryPoint.path, format);
|
||||
const markerExists = existsSync(markerPath);
|
||||
if (markerExists) {
|
||||
const previousVersion = readFileSync(markerPath, 'utf8');
|
||||
if (previousVersion !== NGCC_VERSION) {
|
||||
throw new Error(
|
||||
'The ngcc compiler has changed since the last ngcc build.\n' +
|
||||
'Please completely remove `node_modules` and try again.');
|
||||
}
|
||||
}
|
||||
return markerExists;
|
||||
}
|
||||
|
||||
export function writeMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat) {
|
||||
const markerPath = getMarkerPath(entryPoint.path, format);
|
||||
writeFileSync(markerPath, NGCC_VERSION, 'utf8');
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as path from 'canonical-path';
|
||||
import * as fs from 'fs';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Helper functions for computing dependencies.
|
||||
*/
|
||||
export class DependencyHost {
|
||||
/**
|
||||
* Get a list of the resolved paths to all the dependencies of this entry point.
|
||||
* @param from An absolute path to the file whose dependencies we want to get.
|
||||
* @param resolved A set that will have the resolved dependencies added to it.
|
||||
* @param missing A set that will have the dependencies that could not be found added to it.
|
||||
* @param internal A set that is used to track internal dependencies to prevent getting stuck in a
|
||||
* circular dependency loop.
|
||||
* @returns an object containing an array of absolute paths to `resolved` depenendencies and an
|
||||
* array of import specifiers for dependencies that were `missing`.
|
||||
*/
|
||||
computeDependencies(
|
||||
from: string, resolved: Set<string>, missing: Set<string>,
|
||||
internal: Set<string> = new Set()): void {
|
||||
const fromContents = fs.readFileSync(from, 'utf8');
|
||||
if (!this.hasImportOrReeportStatements(fromContents)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
|
||||
const sf =
|
||||
ts.createSourceFile(from, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
|
||||
sf.statements
|
||||
// filter out statements that are not imports or reexports
|
||||
.filter(this.isStringImportOrReexport)
|
||||
// Grab the id of the module that is being imported
|
||||
.map(stmt => stmt.moduleSpecifier.text)
|
||||
// Resolve this module id into an absolute path
|
||||
.forEach(importPath => {
|
||||
if (importPath.startsWith('.')) {
|
||||
// This is an internal import so follow it
|
||||
const internalDependency = this.resolveInternal(from, importPath);
|
||||
// Avoid circular dependencies
|
||||
if (!internal.has(internalDependency)) {
|
||||
internal.add(internalDependency);
|
||||
this.computeDependencies(internalDependency, resolved, missing, internal);
|
||||
}
|
||||
} else {
|
||||
const externalDependency = this.tryResolveExternal(from, importPath);
|
||||
if (externalDependency !== null) {
|
||||
resolved.add(externalDependency);
|
||||
} else {
|
||||
missing.add(importPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an internal module import.
|
||||
* @param from the absolute file path from where to start trying to resolve this module
|
||||
* @param to the module specifier of the internal dependency to resolve
|
||||
* @returns the resolved path to the import.
|
||||
*/
|
||||
resolveInternal(from: string, to: string): string {
|
||||
const fromDirectory = path.dirname(from);
|
||||
// `fromDirectory` is absolute so we don't need to worry about telling `require.resolve`
|
||||
// about it - unlike `tryResolve` below.
|
||||
return require.resolve(path.resolve(fromDirectory, to));
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't want to resolve external dependencies directly because if it is a path to a
|
||||
* sub-entry-point (e.g. @angular/animations/browser rather than @angular/animations)
|
||||
* then `require.resolve()` may return a path to a UMD bundle, which may actually live
|
||||
* in the folder containing the sub-entry-point
|
||||
* (e.g. @angular/animations/bundles/animations-browser.umd.js).
|
||||
*
|
||||
* Instead we try to resolve it as a package, which is what we would need anyway for it to be
|
||||
* compilable by ngcc.
|
||||
*
|
||||
* If `to` is actually a path to a file then this will fail, which is what we want.
|
||||
*
|
||||
* @param from the file path from where to start trying to resolve this module
|
||||
* @param to the module specifier of the dependency to resolve
|
||||
* @returns the resolved path to the entry point directory of the import or null
|
||||
* if it cannot be resolved.
|
||||
*/
|
||||
tryResolveExternal(from: string, to: string): string|null {
|
||||
const externalDependency = this.tryResolve(from, `${to}/package.json`);
|
||||
return externalDependency && path.dirname(externalDependency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the absolute path of a module from a particular starting point.
|
||||
*
|
||||
* @param from the file path from where to start trying to resolve this module
|
||||
* @param to the module specifier of the dependency to resolve
|
||||
* @returns an absolute path to the entry-point of the dependency or null if it could not be
|
||||
* resolved.
|
||||
*/
|
||||
tryResolve(from: string, to: string): string|null {
|
||||
try {
|
||||
return require.resolve(to, {paths: [from]});
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given statement is an import with a string literal module specifier.
|
||||
* @param stmt the statement node to check.
|
||||
* @returns true if the statement is an import with a string literal module specifier.
|
||||
*/
|
||||
isStringImportOrReexport(stmt: ts.Statement): stmt is ts.ImportDeclaration&
|
||||
{moduleSpecifier: ts.StringLiteral} {
|
||||
return ts.isImportDeclaration(stmt) ||
|
||||
ts.isExportDeclaration(stmt) && !!stmt.moduleSpecifier &&
|
||||
ts.isStringLiteral(stmt.moduleSpecifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a source file needs to be parsed for imports.
|
||||
* This is a performance short-circuit, which saves us from creating
|
||||
* a TypeScript AST unnecessarily.
|
||||
*
|
||||
* @param source The content of the source file to check.
|
||||
*
|
||||
* @returns false if there are definitely no import or re-export statements
|
||||
* in this file, true otherwise.
|
||||
*/
|
||||
hasImportOrReeportStatements(source: string): boolean {
|
||||
return /(import|export)\s.+from/.test(source);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DepGraph} from 'dependency-graph';
|
||||
import {DependencyHost} from './dependency_host';
|
||||
import {EntryPoint} from './entry_point';
|
||||
|
||||
|
||||
/**
|
||||
* Holds information about entry points that are removed because
|
||||
* they have dependencies that are missing (directly or transitively).
|
||||
*
|
||||
* This might not be an error, because such an entry point might not actually be used
|
||||
* in the application. If it is used then the `ngc` application compilation would
|
||||
* fail also, so we don't need ngcc to catch this.
|
||||
*
|
||||
* For example, consider an application that uses the `@angular/router` package.
|
||||
* This package includes an entry-point called `@angular/router/upgrade`, which has a dependency
|
||||
* on the `@angular/upgrade` package.
|
||||
* If the application never uses code from `@angular/router/upgrade` then there is no need for
|
||||
* `@angular/upgrade` to be installed.
|
||||
* In this case the ngcc tool should just ignore the `@angular/router/upgrade` end-point.
|
||||
*/
|
||||
export interface InvalidEntryPoint {
|
||||
entryPoint: EntryPoint;
|
||||
missingDependencies: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds information about dependencies of an entry-point that do not need to be processed
|
||||
* by the ngcc tool.
|
||||
*
|
||||
* For example, the `rxjs` package does not contain any Angular decorators that need to be
|
||||
* compiled and so this can be safely ignored by ngcc.
|
||||
*/
|
||||
export interface IgnoredDependency {
|
||||
entryPoint: EntryPoint;
|
||||
dependencyPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of sorting the entry-points by their dependencies.
|
||||
*
|
||||
* The `entryPoints` array will be ordered so that no entry point depends upon an entry point that
|
||||
* appears later in the array.
|
||||
*
|
||||
* Some entry points or their dependencies may be have been ignored. These are captured for
|
||||
* diagnostic purposes in `invalidEntryPoints` and `ignoredDependencies` respectively.
|
||||
*/
|
||||
export interface SortedEntryPointsInfo {
|
||||
entryPoints: EntryPoint[];
|
||||
invalidEntryPoints: InvalidEntryPoint[];
|
||||
ignoredDependencies: IgnoredDependency[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that resolves dependencies between entry-points.
|
||||
*/
|
||||
export class DependencyResolver {
|
||||
constructor(private host: DependencyHost) {}
|
||||
/**
|
||||
* Sort the array of entry points so that the dependant entry points always come later than
|
||||
* their dependencies in the array.
|
||||
* @param entryPoints An array entry points to sort.
|
||||
* @returns the result of sorting the entry points.
|
||||
*/
|
||||
sortEntryPointsByDependency(entryPoints: EntryPoint[]): SortedEntryPointsInfo {
|
||||
const invalidEntryPoints: InvalidEntryPoint[] = [];
|
||||
const ignoredDependencies: IgnoredDependency[] = [];
|
||||
const graph = new DepGraph<EntryPoint>();
|
||||
|
||||
// Add the entry ponts to the graph as nodes
|
||||
entryPoints.forEach(entryPoint => graph.addNode(entryPoint.path, entryPoint));
|
||||
|
||||
// Now add the dependencies between them
|
||||
entryPoints.forEach(entryPoint => {
|
||||
const entryPointPath = entryPoint.esm2015;
|
||||
if (!entryPointPath) {
|
||||
throw new Error(`Esm2015 format missing in '${entryPoint.path}' entry-point.`);
|
||||
}
|
||||
|
||||
const dependencies = new Set<string>();
|
||||
const missing = new Set<string>();
|
||||
this.host.computeDependencies(entryPointPath, dependencies, missing);
|
||||
|
||||
if (missing.size > 0) {
|
||||
// This entry point has dependencies that are missing
|
||||
// so remove it from the graph.
|
||||
removeNodes(entryPoint, Array.from(missing));
|
||||
} else {
|
||||
dependencies.forEach(dependencyPath => {
|
||||
if (graph.hasNode(dependencyPath)) {
|
||||
// The dependency path maps to an entry point that exists in the graph
|
||||
// so add the dependency.
|
||||
graph.addDependency(entryPoint.path, dependencyPath);
|
||||
} else if (invalidEntryPoints.some(i => i.entryPoint.path === dependencyPath)) {
|
||||
// The dependency path maps to an entry-point that was previously removed
|
||||
// from the graph, so remove this entry-point as well.
|
||||
removeNodes(entryPoint, [dependencyPath]);
|
||||
} else {
|
||||
// The dependency path points to a package that ngcc does not care about.
|
||||
ignoredDependencies.push({entryPoint, dependencyPath});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// The map now only holds entry-points that ngcc cares about and whose dependencies
|
||||
// (direct and transitive) all exist.
|
||||
return {
|
||||
entryPoints: graph.overallOrder().map(path => graph.getNodeData(path)),
|
||||
invalidEntryPoints,
|
||||
ignoredDependencies
|
||||
};
|
||||
|
||||
function removeNodes(entryPoint: EntryPoint, missingDependencies: string[]) {
|
||||
const nodesToRemove = [entryPoint.path, ...graph.dependantsOf(entryPoint.path)];
|
||||
nodesToRemove.forEach(node => {
|
||||
invalidEntryPoints.push({entryPoint: graph.getNodeData(node), missingDependencies});
|
||||
graph.removeNode(node);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as path from 'canonical-path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
|
||||
/**
|
||||
* The possible values for the format of an entry-point.
|
||||
*/
|
||||
export type EntryPointFormat = 'esm5' | 'fesm5' | 'esm2015' | 'fesm2015' | 'umd';
|
||||
|
||||
/**
|
||||
* An object containing paths to the entry-points for each format.
|
||||
*/
|
||||
export type EntryPointPaths = {
|
||||
[Format in EntryPointFormat]?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* An object containing information about an entry-point, including paths
|
||||
* to each of the possible entry-point formats.
|
||||
*/
|
||||
export type EntryPoint = EntryPointPaths & {
|
||||
/** The path to the package that contains this entry-point. */
|
||||
package: string;
|
||||
/** The path to this entry point. */
|
||||
path: string;
|
||||
/** The path to a typings (.d.ts) file for this entry-point. */
|
||||
typings: string;
|
||||
};
|
||||
|
||||
interface EntryPointPackageJson {
|
||||
fesm2015?: string;
|
||||
fesm5?: string;
|
||||
esm2015?: string;
|
||||
esm5?: string;
|
||||
main?: string;
|
||||
types?: string;
|
||||
typings?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get entry point info from the given path.
|
||||
* @param pkgPath the absolute path to the containing npm package
|
||||
* @param entryPoint the absolute path to the potential entry point.
|
||||
* @returns Info about the entry point if it is valid, `null` otherwise.
|
||||
*/
|
||||
export function getEntryPointInfo(pkgPath: string, entryPoint: string): EntryPoint|null {
|
||||
const packageJsonPath = path.resolve(entryPoint, 'package.json');
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// According to https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html,
|
||||
// `types` and `typings` are interchangeable.
|
||||
const {fesm2015, fesm5, esm2015, esm5, main, types, typings = types}: EntryPointPackageJson =
|
||||
JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
// Minimum requirement is that we have esm2015 format and typings.
|
||||
if (!typings || !esm2015) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Also we need to have a metadata.json file
|
||||
const metadataPath = path.resolve(entryPoint, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
|
||||
if (!fs.existsSync(metadataPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entryPointInfo: EntryPoint = {
|
||||
package: pkgPath,
|
||||
path: entryPoint,
|
||||
typings: path.resolve(entryPoint, typings),
|
||||
esm2015: path.resolve(entryPoint, esm2015),
|
||||
};
|
||||
|
||||
if (fesm2015) {
|
||||
entryPointInfo.fesm2015 = path.resolve(entryPoint, fesm2015);
|
||||
}
|
||||
if (fesm5) {
|
||||
entryPointInfo.fesm5 = path.resolve(entryPoint, fesm5);
|
||||
}
|
||||
if (esm5) {
|
||||
entryPointInfo.esm5 = path.resolve(entryPoint, esm5);
|
||||
}
|
||||
if (main) {
|
||||
entryPointInfo.umd = path.resolve(entryPoint, main);
|
||||
}
|
||||
|
||||
return entryPointInfo;
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as path from 'canonical-path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import {DependencyResolver, SortedEntryPointsInfo} from './dependency_resolver';
|
||||
import {EntryPoint, getEntryPointInfo} from './entry_point';
|
||||
|
||||
|
||||
export class EntryPointFinder {
|
||||
constructor(private resolver: DependencyResolver) {}
|
||||
/**
|
||||
* Search the given directory, and sub-directories, for Angular package entry points.
|
||||
* @param sourceDirectory An absolute path to the directory to search for entry points.
|
||||
*/
|
||||
findEntryPoints(sourceDirectory: string): SortedEntryPointsInfo {
|
||||
const unsortedEntryPoints = walkDirectoryForEntryPoints(sourceDirectory);
|
||||
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for entry points that need to be compiled, starting at the source directory.
|
||||
* The function will recurse into directories that start with `@...`, e.g. `@angular/...`.
|
||||
* @param sourceDirectory An absolute path to the root directory where searching begins.
|
||||
*/
|
||||
function walkDirectoryForEntryPoints(sourceDirectory: string): EntryPoint[] {
|
||||
const entryPoints: EntryPoint[] = [];
|
||||
fs.readdirSync(sourceDirectory)
|
||||
// Not interested in hidden files
|
||||
.filter(p => !p.startsWith('.'))
|
||||
// Ignore node_modules
|
||||
.filter(p => p !== 'node_modules')
|
||||
// Only interested in directories (and only those that are not symlinks)
|
||||
.filter(p => {
|
||||
const stat = fs.lstatSync(path.resolve(sourceDirectory, p));
|
||||
return stat.isDirectory() && !stat.isSymbolicLink();
|
||||
})
|
||||
.forEach(p => {
|
||||
// Either the directory is a potential package or a namespace containing packages (e.g
|
||||
// `@angular`).
|
||||
const packagePath = path.join(sourceDirectory, p);
|
||||
if (p.startsWith('@')) {
|
||||
entryPoints.push(...walkDirectoryForEntryPoints(packagePath));
|
||||
} else {
|
||||
entryPoints.push(...getEntryPointsForPackage(packagePath));
|
||||
|
||||
// Also check for any nested node_modules in this package
|
||||
const nestedNodeModulesPath = path.resolve(packagePath, 'node_modules');
|
||||
if (fs.existsSync(nestedNodeModulesPath)) {
|
||||
entryPoints.push(...walkDirectoryForEntryPoints(nestedNodeModulesPath));
|
||||
}
|
||||
}
|
||||
});
|
||||
return entryPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurse the folder structure looking for all the entry points
|
||||
* @param packagePath The absolute path to an npm package that may contain entry points
|
||||
* @returns An array of entry points that were discovered.
|
||||
*/
|
||||
function getEntryPointsForPackage(packagePath: string): EntryPoint[] {
|
||||
const entryPoints: EntryPoint[] = [];
|
||||
|
||||
// Try to get an entry point from the top level package directory
|
||||
const topLevelEntryPoint = getEntryPointInfo(packagePath, packagePath);
|
||||
if (topLevelEntryPoint !== null) {
|
||||
entryPoints.push(topLevelEntryPoint);
|
||||
}
|
||||
|
||||
// Now search all the directories of this package for possible entry points
|
||||
walkDirectory(packagePath, subdir => {
|
||||
const subEntryPoint = getEntryPointInfo(packagePath, subdir);
|
||||
if (subEntryPoint !== null) {
|
||||
entryPoints.push(subEntryPoint);
|
||||
}
|
||||
});
|
||||
|
||||
return entryPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively walk a directory and its sub-directories, applying a given
|
||||
* function to each directory.
|
||||
* @param dir the directory to recursively walk.
|
||||
* @param fn the function to apply to each directory.
|
||||
*/
|
||||
function walkDirectory(dir: string, fn: (dir: string) => void) {
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
// Not interested in hidden files
|
||||
.filter(p => !p.startsWith('.'))
|
||||
// Ignore node_modules
|
||||
.filter(p => p !== 'node_modules')
|
||||
// Only interested in directories (and only those that are not symlinks)
|
||||
.filter(p => {
|
||||
const stat = fs.lstatSync(path.resolve(dir, p));
|
||||
return stat.isDirectory() && !stat.isSymbolicLink();
|
||||
})
|
||||
.forEach(subdir => {
|
||||
subdir = path.resolve(dir, subdir);
|
||||
fn(subdir);
|
||||
walkDirectory(subdir, fn);
|
||||
});
|
||||
}
|
|
@ -24,8 +24,8 @@ import {FileParser} from '../parsing/file_parser';
|
|||
import {Esm2015Renderer} from '../rendering/esm2015_renderer';
|
||||
import {Esm5Renderer} from '../rendering/esm5_renderer';
|
||||
import {FileInfo, Renderer} from '../rendering/renderer';
|
||||
|
||||
import {checkMarkerFile, findAllPackageJsonFiles, getEntryPoints, writeMarkerFile} from './utils';
|
||||
import {checkMarkerFile, writeMarkerFile} from './build_marker';
|
||||
import {EntryPoint, EntryPointFormat} from './entry_point';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -47,67 +47,68 @@ import {checkMarkerFile, findAllPackageJsonFiles, getEntryPoints, writeMarkerFil
|
|||
* - Other packages may re-export classes from other non-entry point files.
|
||||
* - Some formats may contain multiple "modules" in a single file.
|
||||
*/
|
||||
export class PackageTransformer {
|
||||
transform(packagePath: string, format: string, targetPath: string = 'node_modules'): void {
|
||||
const sourceNodeModules = this.findNodeModulesPath(packagePath);
|
||||
const targetNodeModules = resolve(sourceNodeModules, '..', targetPath);
|
||||
const packageJsonPaths =
|
||||
findAllPackageJsonFiles(packagePath)
|
||||
// Ignore paths that have been built already
|
||||
.filter(packageJsonPath => !checkMarkerFile(packageJsonPath, format));
|
||||
const entryPoints = getEntryPoints(packageJsonPaths, format);
|
||||
export class Transformer {
|
||||
constructor(private sourcePath: string, private targetPath: string) {}
|
||||
|
||||
entryPoints.forEach(entryPoint => {
|
||||
const outputFiles: FileInfo[] = [];
|
||||
const options: ts.CompilerOptions = {
|
||||
allowJs: true,
|
||||
maxNodeModuleJsDepth: Infinity,
|
||||
rootDir: entryPoint.entryFileName,
|
||||
};
|
||||
transform(entryPoint: EntryPoint, format: EntryPointFormat): void {
|
||||
if (checkMarkerFile(entryPoint, format)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the TS program and necessary helpers.
|
||||
// TODO : create a custom compiler host that reads from .bak files if available.
|
||||
const host = ts.createCompilerHost(options);
|
||||
let rootDirs: string[]|undefined = undefined;
|
||||
if (options.rootDirs !== undefined) {
|
||||
rootDirs = options.rootDirs;
|
||||
} else if (options.rootDir !== undefined) {
|
||||
rootDirs = [options.rootDir];
|
||||
} else {
|
||||
rootDirs = [host.getCurrentDirectory()];
|
||||
}
|
||||
const packageProgram = ts.createProgram([entryPoint.entryFileName], options, host);
|
||||
const typeChecker = packageProgram.getTypeChecker();
|
||||
const dtsMapper = new DtsMapper(entryPoint.entryRoot, entryPoint.dtsEntryRoot);
|
||||
const reflectionHost = this.getHost(format, packageProgram, dtsMapper);
|
||||
const outputFiles: FileInfo[] = [];
|
||||
const options: ts.CompilerOptions = {
|
||||
allowJs: true,
|
||||
maxNodeModuleJsDepth: Infinity,
|
||||
rootDir: entryPoint.path,
|
||||
};
|
||||
|
||||
const parser = this.getFileParser(format, packageProgram, reflectionHost);
|
||||
const analyzer = new Analyzer(typeChecker, reflectionHost, rootDirs);
|
||||
const renderer = this.getRenderer(format, packageProgram, reflectionHost);
|
||||
// Create the TS program and necessary helpers.
|
||||
// TODO : create a custom compiler host that reads from .bak files if available.
|
||||
const host = ts.createCompilerHost(options);
|
||||
let rootDirs: string[]|undefined = undefined;
|
||||
if (options.rootDirs !== undefined) {
|
||||
rootDirs = options.rootDirs;
|
||||
} else if (options.rootDir !== undefined) {
|
||||
rootDirs = [options.rootDir];
|
||||
} else {
|
||||
rootDirs = [host.getCurrentDirectory()];
|
||||
}
|
||||
const entryPointFilePath = entryPoint[format];
|
||||
if (!entryPointFilePath) {
|
||||
throw new Error(
|
||||
`Missing entry point file for format, ${format}, in package, ${entryPoint.path}.`);
|
||||
}
|
||||
const packageProgram = ts.createProgram([entryPointFilePath], options, host);
|
||||
const typeChecker = packageProgram.getTypeChecker();
|
||||
const dtsMapper = new DtsMapper(dirname(entryPointFilePath), dirname(entryPoint.typings));
|
||||
const reflectionHost = this.getHost(format, packageProgram, dtsMapper);
|
||||
|
||||
// Parse and analyze the files.
|
||||
const entryPointFile = packageProgram.getSourceFile(entryPoint.entryFileName) !;
|
||||
const parsedFiles = parser.parseFile(entryPointFile);
|
||||
const analyzedFiles = parsedFiles.map(parsedFile => analyzer.analyzeFile(parsedFile));
|
||||
const parser = this.getFileParser(format, packageProgram, reflectionHost);
|
||||
const analyzer = new Analyzer(typeChecker, reflectionHost, rootDirs);
|
||||
const renderer = this.getRenderer(format, packageProgram, reflectionHost);
|
||||
|
||||
// Transform the source files and source maps.
|
||||
outputFiles.push(...this.transformSourceFiles(
|
||||
analyzedFiles, sourceNodeModules, targetNodeModules, renderer));
|
||||
// Parse and analyze the files.
|
||||
const entryPointFile = packageProgram.getSourceFile(entryPointFilePath) !;
|
||||
const parsedFiles = parser.parseFile(entryPointFile);
|
||||
const analyzedFiles = parsedFiles.map(parsedFile => analyzer.analyzeFile(parsedFile));
|
||||
|
||||
// Transform the `.d.ts` files (if necessary).
|
||||
// TODO(gkalpak): What about `.d.ts` source maps? (See
|
||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#new---declarationmap.)
|
||||
if (format === 'esm2015') {
|
||||
outputFiles.push(...this.transformDtsFiles(
|
||||
analyzedFiles, sourceNodeModules, targetNodeModules, dtsMapper));
|
||||
}
|
||||
// Transform the source files and source maps.
|
||||
outputFiles.push(
|
||||
...this.transformSourceFiles(analyzedFiles, this.sourcePath, this.targetPath, renderer));
|
||||
|
||||
// Write out all the transformed files.
|
||||
outputFiles.forEach(file => this.writeFile(file));
|
||||
});
|
||||
// Transform the `.d.ts` files (if necessary).
|
||||
// TODO(gkalpak): What about `.d.ts` source maps? (See
|
||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#new---declarationmap.)
|
||||
if (format === 'esm2015') {
|
||||
outputFiles.push(
|
||||
...this.transformDtsFiles(analyzedFiles, this.sourcePath, this.targetPath, dtsMapper));
|
||||
}
|
||||
|
||||
// Write the built-with-ngcc markers
|
||||
packageJsonPaths.forEach(packageJsonPath => { writeMarkerFile(packageJsonPath, format); });
|
||||
// Write out all the transformed files.
|
||||
outputFiles.forEach(file => this.writeFile(file));
|
||||
|
||||
// Write the built-with-ngcc marker
|
||||
writeMarkerFile(entryPoint, format);
|
||||
}
|
||||
|
||||
getHost(format: string, program: ts.Program, dtsMapper: DtsMapper): NgccReflectionHost {
|
||||
|
@ -150,13 +151,6 @@ export class PackageTransformer {
|
|||
}
|
||||
}
|
||||
|
||||
findNodeModulesPath(src: string): string {
|
||||
while (src && !/node_modules$/.test(src)) {
|
||||
src = dirname(src);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
transformDtsFiles(
|
||||
analyzedFiles: AnalyzedFile[], sourceNodeModules: string, targetNodeModules: string,
|
||||
dtsMapper: DtsMapper): FileInfo[] {
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {existsSync, readFileSync, writeFileSync} from 'fs';
|
||||
import * as mockFs from 'mock-fs';
|
||||
|
||||
import {checkMarkerFile, writeMarkerFile} from '../../src/packages/build_marker';
|
||||
import {EntryPoint} from '../../src/packages/entry_point';
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/node_modules/@angular/common': {
|
||||
'package.json': `{
|
||||
"fesm2015": "./fesm2015/common.js",
|
||||
"fesm5": "./fesm5/common.js",
|
||||
"typings": "./common.d.ts"
|
||||
}`,
|
||||
'fesm2015': {
|
||||
'common.js': 'DUMMY CONTENT',
|
||||
'http.js': 'DUMMY CONTENT',
|
||||
'http/testing.js': 'DUMMY CONTENT',
|
||||
'testing.js': 'DUMMY CONTENT',
|
||||
},
|
||||
'http': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../fesm2015/http.js",
|
||||
"fesm5": "../fesm5/http.js",
|
||||
"typings": "./http.d.ts"
|
||||
}`,
|
||||
'testing': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../../fesm2015/http/testing.js",
|
||||
"fesm5": "../../fesm5/http/testing.js",
|
||||
"typings": "../http/testing.d.ts"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
'other': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
'testing': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../fesm2015/testing.js",
|
||||
"fesm5": "../fesm5/testing.js",
|
||||
"typings": "../testing.d.ts"
|
||||
}`,
|
||||
},
|
||||
'node_modules': {
|
||||
'tslib': {
|
||||
'package.json': '{ }',
|
||||
'node_modules': {
|
||||
'other-lib': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/node_modules/@angular/no-typings': {
|
||||
'package.json': `{
|
||||
"fesm2015": "./fesm2015/index.js"
|
||||
}`,
|
||||
'fesm2015': {
|
||||
'index.js': 'DUMMY CONTENT',
|
||||
'index.d.ts': 'DUMMY CONTENT',
|
||||
},
|
||||
},
|
||||
'/node_modules/@angular/other': {
|
||||
'not-package.json': '{ "fesm2015": "./fesm2015/other.js" }',
|
||||
'package.jsonot': '{ "fesm5": "./fesm5/other.js" }',
|
||||
},
|
||||
'/node_modules/@angular/other2': {
|
||||
'node_modules_not': {
|
||||
'lib1': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
},
|
||||
'not_node_modules': {
|
||||
'lib2': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function restoreRealFileSystem() {
|
||||
mockFs.restore();
|
||||
}
|
||||
|
||||
function createEntryPoint(path: string): EntryPoint {
|
||||
return {path, package: '', typings: ''};
|
||||
}
|
||||
|
||||
describe('Marker files', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
describe('writeMarkerFile', () => {
|
||||
it('should write a file containing the version placeholder', () => {
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(false);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
|
||||
|
||||
writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015');
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(true);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
|
||||
expect(
|
||||
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
|
||||
writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'esm5');
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(true);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(true);
|
||||
expect(
|
||||
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
expect(readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkMarkerFile', () => {
|
||||
it('should return false if the marker file does not exist', () => {
|
||||
expect(checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if the marker file exists and contains the correct version', () => {
|
||||
writeFileSync(
|
||||
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', '0.0.0-PLACEHOLDER',
|
||||
'utf8');
|
||||
expect(checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should throw if the marker file exists but contains the wrong version', () => {
|
||||
writeFileSync(
|
||||
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'WRONG_VERSION',
|
||||
'utf8');
|
||||
expect(() => checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
|
||||
.toThrowError(
|
||||
'The ngcc compiler has changed since the last ngcc build.\n' +
|
||||
'Please completely remove `node_modules` and try again.');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,224 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as path from 'canonical-path';
|
||||
import * as mockFs from 'mock-fs';
|
||||
import * as ts from 'typescript';
|
||||
import {DependencyHost} from '../../src/packages/dependency_host';
|
||||
const Module = require('module');
|
||||
|
||||
interface DepMap {
|
||||
[path: string]: {resolved: string[], missing: string[]};
|
||||
}
|
||||
|
||||
describe('DependencyHost', () => {
|
||||
let host: DependencyHost;
|
||||
beforeEach(() => host = new DependencyHost());
|
||||
|
||||
describe('getDependencies()', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should not generate a TS AST if the source does not contain any imports or re-exports',
|
||||
() => {
|
||||
spyOn(ts, 'createSourceFile');
|
||||
host.computeDependencies('/no/imports/or/re-exports.js', new Set(), new Set());
|
||||
expect(ts.createSourceFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should resolve all the external imports of the source file', () => {
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/external/imports.js', resolved, missing);
|
||||
expect(resolved.size).toBe(2);
|
||||
expect(resolved.has('RESOLVED/path/to/x'));
|
||||
expect(resolved.has('RESOLVED/path/to/y'));
|
||||
});
|
||||
|
||||
it('should resolve all the external re-exports of the source file', () => {
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/external/re-exports.js', resolved, missing);
|
||||
expect(resolved.size).toBe(2);
|
||||
expect(resolved.has('RESOLVED/path/to/x'));
|
||||
expect(resolved.has('RESOLVED/path/to/y'));
|
||||
});
|
||||
|
||||
it('should capture missing external imports', () => {
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake(
|
||||
(from: string, importPath: string) =>
|
||||
importPath === 'missing' ? null : `RESOLVED/${importPath}`);
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/external/imports-missing.js', resolved, missing);
|
||||
expect(resolved.size).toBe(1);
|
||||
expect(resolved.has('RESOLVED/path/to/x'));
|
||||
expect(missing.size).toBe(1);
|
||||
expect(missing.has('missing'));
|
||||
});
|
||||
|
||||
it('should recurse into internal dependencies', () => {
|
||||
spyOn(host, 'resolveInternal')
|
||||
.and.callFake(
|
||||
(from: string, importPath: string) => path.join('/internal', importPath + '.js'));
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
|
||||
const getDependenciesSpy = spyOn(host, 'computeDependencies').and.callThrough();
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/internal/outer.js', resolved, missing);
|
||||
expect(getDependenciesSpy).toHaveBeenCalledWith('/internal/outer.js', resolved, missing);
|
||||
expect(getDependenciesSpy)
|
||||
.toHaveBeenCalledWith('/internal/inner.js', resolved, missing, jasmine.any(Set));
|
||||
expect(resolved.size).toBe(1);
|
||||
expect(resolved.has('RESOLVED/path/to/y'));
|
||||
});
|
||||
|
||||
|
||||
it('should handle circular internal dependencies', () => {
|
||||
spyOn(host, 'resolveInternal')
|
||||
.and.callFake(
|
||||
(from: string, importPath: string) => path.join('/internal', importPath + '.js'));
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/internal/circular-a.js', resolved, missing);
|
||||
expect(resolved.size).toBe(2);
|
||||
expect(resolved.has('RESOLVED/path/to/x'));
|
||||
expect(resolved.has('RESOLVED/path/to/y'));
|
||||
});
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/no/imports/or/re-exports.js': 'some text but no import-like statements',
|
||||
'/external/imports.js': `import {X} from 'path/to/x';\nimport {Y} from 'path/to/y';`,
|
||||
'/external/re-exports.js': `export {X} from 'path/to/x';\nexport {Y} from 'path/to/y';`,
|
||||
'/external/imports-missing.js': `import {X} from 'path/to/x';\nimport {Y} from 'missing';`,
|
||||
'/internal/outer.js': `import {X} from './inner';`,
|
||||
'/internal/inner.js': `import {Y} from 'path/to/y';`,
|
||||
'/internal/circular-a.js': `import {B} from './circular-b'; import {X} from 'path/to/x';`,
|
||||
'/internal/circular-b.js': `import {A} from './circular-a'; import {Y} from 'path/to/y';`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('resolveInternal', () => {
|
||||
it('should resolve the dependency via `Module._resolveFilename`', () => {
|
||||
spyOn(Module, '_resolveFilename').and.returnValue('RESOLVED_PATH');
|
||||
const result = host.resolveInternal('/SOURCE/PATH/FILE', '../TARGET/PATH/FILE');
|
||||
expect(result).toEqual('RESOLVED_PATH');
|
||||
});
|
||||
|
||||
it('should first resolve the `to` on top of the `from` directory', () => {
|
||||
const resolveSpy = spyOn(Module, '_resolveFilename').and.returnValue('RESOLVED_PATH');
|
||||
host.resolveInternal('/SOURCE/PATH/FILE', '../TARGET/PATH/FILE');
|
||||
expect(resolveSpy)
|
||||
.toHaveBeenCalledWith('/SOURCE/TARGET/PATH/FILE', jasmine.any(Object), false, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tryResolveExternal', () => {
|
||||
it('should call `tryResolve`, appending `package.json` to the target path', () => {
|
||||
const tryResolveSpy = spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED');
|
||||
host.tryResolveExternal('SOURCE_PATH', 'TARGET_PATH');
|
||||
expect(tryResolveSpy).toHaveBeenCalledWith('SOURCE_PATH', 'TARGET_PATH/package.json');
|
||||
});
|
||||
|
||||
it('should return the directory containing the result from `tryResolve', () => {
|
||||
spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED');
|
||||
expect(host.tryResolveExternal('SOURCE_PATH', 'TARGET_PATH')).toEqual('PATH/TO');
|
||||
});
|
||||
|
||||
it('should return null if `tryResolve` returns null', () => {
|
||||
spyOn(host, 'tryResolve').and.returnValue(null);
|
||||
expect(host.tryResolveExternal('SOURCE_PATH', 'TARGET_PATH')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tryResolve()', () => {
|
||||
it('should resolve the dependency via `Module._resolveFilename`, passing the `from` path to the `paths` option',
|
||||
() => {
|
||||
const resolveSpy = spyOn(Module, '_resolveFilename').and.returnValue('RESOLVED_PATH');
|
||||
const result = host.tryResolve('SOURCE_PATH', 'TARGET_PATH');
|
||||
expect(resolveSpy).toHaveBeenCalledWith('TARGET_PATH', jasmine.any(Object), false, {
|
||||
paths: ['SOURCE_PATH']
|
||||
});
|
||||
expect(result).toEqual('RESOLVED_PATH');
|
||||
});
|
||||
|
||||
it('should return null if `Module._resolveFilename` throws an error', () => {
|
||||
const resolveSpy =
|
||||
spyOn(Module, '_resolveFilename').and.throwError(`Cannot find module 'TARGET_PATH'`);
|
||||
const result = host.tryResolve('SOURCE_PATH', 'TARGET_PATH');
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStringImportOrReexport', () => {
|
||||
it('should return true if the statement is an import', () => {
|
||||
expect(host.isStringImportOrReexport(createStatement('import {X} from "some/x";')))
|
||||
.toBe(true);
|
||||
expect(host.isStringImportOrReexport(createStatement('import * as X from "some/x";')))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the statement is a re-export', () => {
|
||||
expect(host.isStringImportOrReexport(createStatement('export {X} from "some/x";')))
|
||||
.toBe(true);
|
||||
expect(host.isStringImportOrReexport(createStatement('export * from "some/x";'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the statement is not an import or a re-export', () => {
|
||||
expect(host.isStringImportOrReexport(createStatement('class X {}'))).toBe(false);
|
||||
expect(host.isStringImportOrReexport(createStatement('export function foo() {}')))
|
||||
.toBe(false);
|
||||
expect(host.isStringImportOrReexport(createStatement('export const X = 10;'))).toBe(false);
|
||||
});
|
||||
|
||||
function createStatement(source: string) {
|
||||
return ts
|
||||
.createSourceFile('source.js', source, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS)
|
||||
.statements[0];
|
||||
}
|
||||
});
|
||||
|
||||
describe('hasImportOrReeportStatements', () => {
|
||||
it('should return true if there is an import statement', () => {
|
||||
expect(host.hasImportOrReeportStatements('import {X} from "some/x";')).toBe(true);
|
||||
expect(host.hasImportOrReeportStatements('import * as X from "some/x";')).toBe(true);
|
||||
expect(
|
||||
host.hasImportOrReeportStatements('blah blah\n\n import {X} from "some/x";\nblah blah'))
|
||||
.toBe(true);
|
||||
expect(host.hasImportOrReeportStatements('\t\timport {X} from "some/x";')).toBe(true);
|
||||
});
|
||||
it('should return true if there is a re-export statement', () => {
|
||||
expect(host.hasImportOrReeportStatements('export {X} from "some/x";')).toBe(true);
|
||||
expect(
|
||||
host.hasImportOrReeportStatements('blah blah\n\n export {X} from "some/x";\nblah blah'))
|
||||
.toBe(true);
|
||||
expect(host.hasImportOrReeportStatements('\t\texport {X} from "some/x";')).toBe(true);
|
||||
expect(host.hasImportOrReeportStatements(
|
||||
'blah blah\n\n export * from "@angular/core;\nblah blah'))
|
||||
.toBe(true);
|
||||
});
|
||||
it('should return false if there is no import nor re-export statement', () => {
|
||||
expect(host.hasImportOrReeportStatements('blah blah')).toBe(false);
|
||||
expect(host.hasImportOrReeportStatements('export function moo() {}')).toBe(false);
|
||||
expect(host.hasImportOrReeportStatements('Some text that happens to include the word import'))
|
||||
.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function restoreRealFileSystem() { mockFs.restore(); }
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DependencyHost} from '../../src/packages/dependency_host';
|
||||
import {DependencyResolver} from '../../src/packages/dependency_resolver';
|
||||
import {EntryPoint} from '../../src/packages/entry_point';
|
||||
|
||||
describe('DepencencyResolver', () => {
|
||||
let host: DependencyHost;
|
||||
let resolver: DependencyResolver;
|
||||
beforeEach(() => {
|
||||
host = new DependencyHost();
|
||||
resolver = new DependencyResolver(host);
|
||||
});
|
||||
describe('sortEntryPointsByDependency()', () => {
|
||||
const first = { path: 'first', esm2015: 'first/index.ts' } as EntryPoint;
|
||||
const second = { path: 'second', esm2015: 'second/index.ts' } as EntryPoint;
|
||||
const third = { path: 'third', esm2015: 'third/index.ts' } as EntryPoint;
|
||||
const fourth = { path: 'fourth', esm2015: 'fourth/index.ts' } as EntryPoint;
|
||||
const fifth = { path: 'fifth', esm2015: 'fifth/index.ts' } as EntryPoint;
|
||||
|
||||
const dependencies = {
|
||||
'first/index.ts': {resolved: ['second', 'third', 'ignored-1'], missing: []},
|
||||
'second/index.ts': {resolved: ['third', 'fifth'], missing: []},
|
||||
'third/index.ts': {resolved: ['fourth', 'ignored-2'], missing: []},
|
||||
'fourth/index.ts': {resolved: ['fifth'], missing: []},
|
||||
'fifth/index.ts': {resolved: [], missing: []},
|
||||
};
|
||||
|
||||
it('should order the entry points by their dependency on each other', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies(dependencies));
|
||||
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
|
||||
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
|
||||
});
|
||||
|
||||
it('should remove entry-points that have missing direct dependencies', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({
|
||||
'first/index.ts': {resolved: [], missing: ['missing']},
|
||||
'second/index.ts': {resolved: [], missing: []},
|
||||
}));
|
||||
const result = resolver.sortEntryPointsByDependency([first, second]);
|
||||
expect(result.entryPoints).toEqual([second]);
|
||||
expect(result.invalidEntryPoints).toEqual([
|
||||
{entryPoint: first, missingDependencies: ['missing']},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove entry points that depended upon an invalid entry-point', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({
|
||||
'first/index.ts': {resolved: ['second'], missing: []},
|
||||
'second/index.ts': {resolved: [], missing: ['missing']},
|
||||
'third/index.ts': {resolved: [], missing: []},
|
||||
}));
|
||||
// Note that we will process `first` before `second`, which has the missing dependency.
|
||||
const result = resolver.sortEntryPointsByDependency([first, second, third]);
|
||||
expect(result.entryPoints).toEqual([third]);
|
||||
expect(result.invalidEntryPoints).toEqual([
|
||||
{entryPoint: second, missingDependencies: ['missing']},
|
||||
{entryPoint: first, missingDependencies: ['missing']},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove entry points that will depend upon an invalid entry-point', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({
|
||||
'first/index.ts': {resolved: ['second'], missing: []},
|
||||
'second/index.ts': {resolved: [], missing: ['missing']},
|
||||
'third/index.ts': {resolved: [], missing: []},
|
||||
}));
|
||||
// Note that we will process `first` after `second`, which has the missing dependency.
|
||||
const result = resolver.sortEntryPointsByDependency([second, first, third]);
|
||||
expect(result.entryPoints).toEqual([third]);
|
||||
expect(result.invalidEntryPoints).toEqual([
|
||||
{entryPoint: second, missingDependencies: ['missing']},
|
||||
{entryPoint: first, missingDependencies: ['second']},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should error if the entry point does not have the esm2015 format', () => {
|
||||
expect(() => resolver.sortEntryPointsByDependency([{ path: 'first' } as EntryPoint]))
|
||||
.toThrowError(`Esm2015 format missing in 'first' entry-point.`);
|
||||
});
|
||||
|
||||
it('should capture any dependencies that were ignored', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies(dependencies));
|
||||
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
|
||||
expect(result.ignoredDependencies).toEqual([
|
||||
{entryPoint: first, dependencyPath: 'ignored-1'},
|
||||
{entryPoint: third, dependencyPath: 'ignored-2'},
|
||||
]);
|
||||
});
|
||||
|
||||
interface DepMap {
|
||||
[path: string]: {resolved: string[], missing: string[]};
|
||||
}
|
||||
|
||||
function createFakeComputeDependencies(dependencies: DepMap) {
|
||||
return (entryPoint: string, resolved: Set<string>, missing: Set<string>) => {
|
||||
dependencies[entryPoint].resolved.forEach(dep => resolved.add(dep));
|
||||
dependencies[entryPoint].missing.forEach(dep => missing.add(dep));
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as mockFs from 'mock-fs';
|
||||
import {DependencyHost} from '../../src/packages/dependency_host';
|
||||
import {DependencyResolver} from '../../src/packages/dependency_resolver';
|
||||
import {EntryPoint} from '../../src/packages/entry_point';
|
||||
import {EntryPointFinder} from '../../src/packages/entry_point_finder';
|
||||
|
||||
describe('findEntryPoints()', () => {
|
||||
let resolver: DependencyResolver;
|
||||
let finder: EntryPointFinder;
|
||||
beforeEach(() => {
|
||||
resolver = new DependencyResolver(new DependencyHost());
|
||||
spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => {
|
||||
return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []};
|
||||
});
|
||||
finder = new EntryPointFinder(resolver);
|
||||
});
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should find sub-entry-points within a package', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/sub_entry_points');
|
||||
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
|
||||
expect(entryPointPaths).toEqual([
|
||||
['/sub_entry_points/common', '/sub_entry_points/common'],
|
||||
['/sub_entry_points/common', '/sub_entry_points/common/http'],
|
||||
['/sub_entry_points/common', '/sub_entry_points/common/http/testing'],
|
||||
['/sub_entry_points/common', '/sub_entry_points/common/testing'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find packages inside a namespace', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/namespaced');
|
||||
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
|
||||
expect(entryPointPaths).toEqual([
|
||||
['/namespaced/@angular/common', '/namespaced/@angular/common'],
|
||||
['/namespaced/@angular/common', '/namespaced/@angular/common/http'],
|
||||
['/namespaced/@angular/common', '/namespaced/@angular/common/http/testing'],
|
||||
['/namespaced/@angular/common', '/namespaced/@angular/common/testing'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no packages', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/no_packages');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no valid entry-points', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/no_valid_entry_points');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should ignore folders starting with .', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/dotted_folders');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should ignore folders that are symlinked', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/symlinked_folders');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle nested node_modules folders', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/nested_node_modules');
|
||||
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
|
||||
expect(entryPointPaths).toEqual([
|
||||
['/nested_node_modules/outer', '/nested_node_modules/outer'],
|
||||
// Note that the inner entry point does not get included as part of the outer package
|
||||
[
|
||||
'/nested_node_modules/outer/node_modules/inner',
|
||||
'/nested_node_modules/outer/node_modules/inner'
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/sub_entry_points': {
|
||||
'common': {
|
||||
'package.json': createPackageJson('common'),
|
||||
'common.metadata.json': 'metadata info',
|
||||
'http': {
|
||||
'package.json': createPackageJson('http'),
|
||||
'http.metadata.json': 'metadata info',
|
||||
'testing': {
|
||||
'package.json': createPackageJson('testing'),
|
||||
'testing.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
'testing': {
|
||||
'package.json': createPackageJson('testing'),
|
||||
'testing.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
},
|
||||
'/namespaced': {
|
||||
'@angular': {
|
||||
'common': {
|
||||
'package.json': createPackageJson('common'),
|
||||
'common.metadata.json': 'metadata info',
|
||||
'http': {
|
||||
'package.json': createPackageJson('http'),
|
||||
'http.metadata.json': 'metadata info',
|
||||
'testing': {
|
||||
'package.json': createPackageJson('testing'),
|
||||
'testing.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
'testing': {
|
||||
'package.json': createPackageJson('testing'),
|
||||
'testing.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/no_packages': {'should_not_be_found': {}},
|
||||
'/no_valid_entry_points': {
|
||||
'some_package': {
|
||||
'package.json': '{}',
|
||||
},
|
||||
},
|
||||
'/dotted_folders': {
|
||||
'.common': {
|
||||
'package.json': createPackageJson('common'),
|
||||
'common.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
'/symlinked_folders': {
|
||||
'common': mockFs.symlink({path: '/sub_entry_points/common'}),
|
||||
},
|
||||
'/nested_node_modules': {
|
||||
'outer': {
|
||||
'package.json': createPackageJson('outer'),
|
||||
'outer.metadata.json': 'metadata info',
|
||||
'node_modules': {
|
||||
'inner': {
|
||||
'package.json': createPackageJson('inner'),
|
||||
'inner.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
function restoreRealFileSystem() { mockFs.restore(); }
|
||||
});
|
||||
|
||||
function createPackageJson(packageName: string): string {
|
||||
const packageJson: any = {
|
||||
typings: `./${packageName}.d.ts`,
|
||||
fesm2015: `./fesm2015/${packageName}.js`,
|
||||
esm2015: `./esm2015/${packageName}.js`,
|
||||
fesm5: `./fesm2015/${packageName}.js`,
|
||||
esm5: `./esm2015/${packageName}.js`,
|
||||
main: `./bundles/${packageName}.umd.js`,
|
||||
};
|
||||
return JSON.stringify(packageJson);
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as mockFs from 'mock-fs';
|
||||
import {getEntryPointInfo} from '../../src/packages/entry_point';
|
||||
|
||||
|
||||
describe('getEntryPointInfo()', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should return an object containing absolute paths to the formats of the specified entry-point',
|
||||
() => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/valid_entry_point');
|
||||
expect(entryPoint).toEqual({
|
||||
package: '/some_package',
|
||||
path: '/some_package/valid_entry_point',
|
||||
typings: `/some_package/valid_entry_point/valid_entry_point.d.ts`,
|
||||
fesm2015: `/some_package/valid_entry_point/fesm2015/valid_entry_point.js`,
|
||||
esm2015: `/some_package/valid_entry_point/esm2015/valid_entry_point.js`,
|
||||
fesm5: `/some_package/valid_entry_point/fesm2015/valid_entry_point.js`,
|
||||
esm5: `/some_package/valid_entry_point/esm2015/valid_entry_point.js`,
|
||||
umd: `/some_package/valid_entry_point/bundles/valid_entry_point.umd.js`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if there is no package.json at the entry-point path', () => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_package_json');
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if there is no typings field in the package.json', () => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_typings');
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if there is no esm2015 field in the package.json', () => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_esm2015');
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if there is no metadata.json file next to the typing file', () => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_metadata.json');
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/some_package': {
|
||||
'valid_entry_point': {
|
||||
'package.json': createPackageJson('valid_entry_point'),
|
||||
'valid_entry_point.metadata.json': 'some meta data',
|
||||
},
|
||||
'missing_package_json': {
|
||||
// no package.json!
|
||||
'missing_package_json.metadata.json': 'some meta data',
|
||||
},
|
||||
'missing_typings': {
|
||||
'package.json': createPackageJson('missing_typings', {exclude: 'typings'}),
|
||||
'missing_typings.metadata.json': 'some meta data',
|
||||
},
|
||||
'missing_esm2015': {
|
||||
'package.json': createPackageJson('missing_esm2015', {exclude: 'esm2015'}),
|
||||
'missing_esm2015.metadata.json': 'some meta data',
|
||||
},
|
||||
'missing_metadata': {
|
||||
'package.json': createPackageJson('missing_metadata'),
|
||||
// no metadata.json!
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function restoreRealFileSystem() {
|
||||
mockFs.restore();
|
||||
}
|
||||
|
||||
function createPackageJson(packageName: string, {exclude}: {exclude?: string} = {}): string {
|
||||
const packageJson: any = {
|
||||
typings: `./${packageName}.d.ts`,
|
||||
fesm2015: `./fesm2015/${packageName}.js`,
|
||||
esm2015: `./esm2015/${packageName}.js`,
|
||||
fesm5: `./fesm2015/${packageName}.js`,
|
||||
esm5: `./esm2015/${packageName}.js`,
|
||||
main: `./bundles/${packageName}.umd.js`,
|
||||
};
|
||||
if (exclude) {
|
||||
delete packageJson[exclude];
|
||||
}
|
||||
return JSON.stringify(packageJson);
|
||||
}
|
|
@ -1,240 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {existsSync, readFileSync, writeFileSync} from 'fs';
|
||||
import * as mockFs from 'mock-fs';
|
||||
|
||||
import {EntryPoint, checkMarkerFile, findAllPackageJsonFiles, getEntryPoints, writeMarkerFile} from '../../src/transform/utils';
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/node_modules/@angular/common': {
|
||||
'package.json': `{
|
||||
"fesm2015": "./fesm2015/common.js",
|
||||
"fesm5": "./fesm5/common.js",
|
||||
"typings": "./common.d.ts"
|
||||
}`,
|
||||
'fesm2015': {
|
||||
'common.js': 'DUMMY CONTENT',
|
||||
'http.js': 'DUMMY CONTENT',
|
||||
'http/testing.js': 'DUMMY CONTENT',
|
||||
'testing.js': 'DUMMY CONTENT',
|
||||
},
|
||||
'http': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../fesm2015/http.js",
|
||||
"fesm5": "../fesm5/http.js",
|
||||
"typings": "./http.d.ts"
|
||||
}`,
|
||||
'testing': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../../fesm2015/http/testing.js",
|
||||
"fesm5": "../../fesm5/http/testing.js",
|
||||
"typings": "../http/testing.d.ts"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
'other': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
'testing': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../fesm2015/testing.js",
|
||||
"fesm5": "../fesm5/testing.js",
|
||||
"typings": "../testing.d.ts"
|
||||
}`,
|
||||
},
|
||||
'node_modules': {
|
||||
'tslib': {
|
||||
'package.json': '{ }',
|
||||
'node_modules': {
|
||||
'other-lib': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/node_modules/@angular/no-typings': {
|
||||
'package.json': `{
|
||||
"fesm2015": "./fesm2015/index.js"
|
||||
}`,
|
||||
'fesm2015': {
|
||||
'index.js': 'DUMMY CONTENT',
|
||||
'index.d.ts': 'DUMMY CONTENT',
|
||||
},
|
||||
},
|
||||
'/node_modules/@angular/other': {
|
||||
'not-package.json': '{ "fesm2015": "./fesm2015/other.js" }',
|
||||
'package.jsonot': '{ "fesm5": "./fesm5/other.js" }',
|
||||
},
|
||||
'/node_modules/@angular/other2': {
|
||||
'node_modules_not': {
|
||||
'lib1': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
},
|
||||
'not_node_modules': {
|
||||
'lib2': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function restoreRealFileSystem() {
|
||||
mockFs.restore();
|
||||
}
|
||||
|
||||
describe('EntryPoint', () => {
|
||||
it('should expose the absolute path to the entry point file', () => {
|
||||
const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts');
|
||||
expect(entryPoint.entryFileName).toBe('/foo/baz/quux.js');
|
||||
});
|
||||
|
||||
it('should expose the package root for the entry point file', () => {
|
||||
const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts');
|
||||
expect(entryPoint.packageRoot).toBe('/foo/bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAllPackageJsonFiles()', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should find the `package.json` files below the specified directory', () => {
|
||||
const paths = findAllPackageJsonFiles('/node_modules/@angular/common');
|
||||
expect(paths.sort()).toEqual([
|
||||
'/node_modules/@angular/common/http/package.json',
|
||||
'/node_modules/@angular/common/http/testing/package.json',
|
||||
'/node_modules/@angular/common/other/package.json',
|
||||
'/node_modules/@angular/common/package.json',
|
||||
'/node_modules/@angular/common/testing/package.json',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not find `package.json` files under `node_modules/`', () => {
|
||||
const paths = findAllPackageJsonFiles('/node_modules/@angular/common');
|
||||
expect(paths).not.toContain('/node_modules/@angular/common/node_modules/tslib/package.json');
|
||||
expect(paths).not.toContain(
|
||||
'/node_modules/@angular/common/node_modules/tslib/node_modules/other-lib/package.json');
|
||||
});
|
||||
|
||||
it('should exactly match the name of `package.json` files', () => {
|
||||
const paths = findAllPackageJsonFiles('/node_modules/@angular/other');
|
||||
expect(paths).toEqual([]);
|
||||
});
|
||||
|
||||
it('should exactly match the name of `node_modules/` directory', () => {
|
||||
const paths = findAllPackageJsonFiles('/node_modules/@angular/other2');
|
||||
expect(paths).toEqual([
|
||||
'/node_modules/@angular/other2/node_modules_not/lib1/package.json',
|
||||
'/node_modules/@angular/other2/not_node_modules/lib2/package.json',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntryPoints()', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should return the entry points for the specified format from each `package.json`', () => {
|
||||
const entryPoints = getEntryPoints(
|
||||
[
|
||||
'/node_modules/@angular/common/package.json',
|
||||
'/node_modules/@angular/common/http/package.json',
|
||||
'/node_modules/@angular/common/http/testing/package.json',
|
||||
'/node_modules/@angular/common/testing/package.json'
|
||||
],
|
||||
'fesm2015');
|
||||
entryPoints.forEach(ep => expect(ep).toEqual(jasmine.any(EntryPoint)));
|
||||
|
||||
const sortedPaths = entryPoints.map(x => x.entryFileName).sort();
|
||||
expect(sortedPaths).toEqual([
|
||||
'/node_modules/@angular/common/fesm2015/common.js',
|
||||
'/node_modules/@angular/common/fesm2015/http.js',
|
||||
'/node_modules/@angular/common/fesm2015/http/testing.js',
|
||||
'/node_modules/@angular/common/fesm2015/testing.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no matching `package.json` files', () => {
|
||||
const entryPoints = getEntryPoints([], 'fesm2015');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no matching formats', () => {
|
||||
const entryPoints = getEntryPoints(['/node_modules/@angular/common/package.json'], 'fesm3000');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an entry point even if the typings are not specified', () => {
|
||||
const entryPoints =
|
||||
getEntryPoints(['/node_modules/@angular/no-typings/package.json'], 'fesm2015');
|
||||
expect(entryPoints.length).toEqual(1);
|
||||
expect(entryPoints[0].entryFileName)
|
||||
.toEqual('/node_modules/@angular/no-typings/fesm2015/index.js');
|
||||
expect(entryPoints[0].entryRoot).toEqual('/node_modules/@angular/no-typings/fesm2015');
|
||||
expect(entryPoints[0].dtsEntryRoot).toEqual(entryPoints[0].entryRoot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Marker files', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
describe('writeMarkerFile', () => {
|
||||
it('should write a file containing the version placeholder', () => {
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(false);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
|
||||
|
||||
writeMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015');
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(true);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
|
||||
expect(
|
||||
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
|
||||
writeMarkerFile('/node_modules/@angular/common/package.json', 'esm5');
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(true);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(true);
|
||||
expect(
|
||||
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
expect(readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkMarkerFile', () => {
|
||||
it('should return false if the marker file does not exist', () => {
|
||||
expect(checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if the marker file exists and contains the correct version', () => {
|
||||
writeFileSync(
|
||||
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', '0.0.0-PLACEHOLDER',
|
||||
'utf8');
|
||||
expect(checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015')).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw if the marker file exists but contains the wrong version', () => {
|
||||
writeFileSync(
|
||||
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'WRONG_VERSION',
|
||||
'utf8');
|
||||
expect(() => checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015'))
|
||||
.toThrowError(
|
||||
'The ngcc compiler has changed since the last ngcc build.\n' +
|
||||
'Please completely remove `node_modules` and try again.');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,6 +9,7 @@
|
|||
import {existsSync, readFileSync, readdirSync, statSync} from 'fs';
|
||||
import * as mockFs from 'mock-fs';
|
||||
import {join} from 'path';
|
||||
const Module = require('module');
|
||||
|
||||
import {mainNgcc} from '../../src/ngcc/src/main';
|
||||
|
||||
|
@ -22,34 +23,31 @@ describe('ngcc main()', () => {
|
|||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should run ngcc without errors for fesm2015', () => {
|
||||
const commonPath = join('/node_modules/@angular/common');
|
||||
const format = 'fesm2015';
|
||||
expect(mainNgcc([format, commonPath])).toBe(0);
|
||||
expect(mainNgcc(['-f', format, '-s', '/node_modules'])).toBe(0);
|
||||
});
|
||||
|
||||
it('should run ngcc without errors for fesm5', () => {
|
||||
const commonPath = join('/node_modules/@angular/common');
|
||||
const format = 'fesm5';
|
||||
expect(mainNgcc([format, commonPath])).toBe(0);
|
||||
expect(mainNgcc(['-f', format, '-s', '/node_modules'])).toBe(0);
|
||||
});
|
||||
|
||||
it('should run ngcc without errors for esm2015', () => {
|
||||
const commonPath = join('/node_modules/@angular/common');
|
||||
const format = 'esm2015';
|
||||
expect(mainNgcc([format, commonPath])).toBe(0);
|
||||
expect(mainNgcc(['-f', format, '-s', '/node_modules'])).toBe(0);
|
||||
});
|
||||
|
||||
it('should run ngcc without errors for esm5', () => {
|
||||
const commonPath = join('/node_modules/@angular/common');
|
||||
const format = 'esm5';
|
||||
expect(mainNgcc([format, commonPath])).toBe(0);
|
||||
expect(mainNgcc(['-f', format, '-s', '/node_modules'])).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function createMockFileSystem() {
|
||||
const packagesPath = join(process.env.TEST_SRCDIR, 'angular/packages');
|
||||
const packagesPath = join(process.env.TEST_SRCDIR !, 'angular/packages');
|
||||
mockFs({'/node_modules/@angular': loadPackages(packagesPath)});
|
||||
spyOn(Module, '_resolveFilename').and.callFake(mockResolve);
|
||||
}
|
||||
|
||||
function restoreRealFileSystem() {
|
||||
|
@ -105,3 +103,23 @@ interface Directory {
|
|||
function isInBazel() {
|
||||
return process.env.TEST_SRCDIR != null;
|
||||
}
|
||||
|
||||
function mockResolve(p: string): string|null {
|
||||
if (existsSync(p)) {
|
||||
const stat = statSync(p);
|
||||
if (stat.isFile()) {
|
||||
return p;
|
||||
} else if (stat.isDirectory()) {
|
||||
const pIndex = mockResolve(p + '/index');
|
||||
if (pIndex && existsSync(pIndex)) {
|
||||
return pIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const ext of ['.js', '.d.ts']) {
|
||||
if (existsSync(p + ext)) {
|
||||
return p + ext;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -58,6 +58,8 @@ filegroup(
|
|||
"braces",
|
||||
"bytebuffer",
|
||||
"cache-base",
|
||||
"camelcase",
|
||||
"canonical-path",
|
||||
"caseless",
|
||||
"chokidar",
|
||||
"class-utils",
|
||||
|
@ -70,10 +72,13 @@ filegroup(
|
|||
"copy-descriptor",
|
||||
"core-util-is",
|
||||
"debug",
|
||||
"decamelize",
|
||||
"decode-uri-component",
|
||||
"define-property",
|
||||
"delayed-stream",
|
||||
"dependency-graph",
|
||||
"domino",
|
||||
"error-ex",
|
||||
"expand-brackets",
|
||||
"expand-range",
|
||||
"extend",
|
||||
|
@ -84,12 +89,14 @@ filegroup(
|
|||
"fast-json-stable-stringify",
|
||||
"filename-regex",
|
||||
"fill-range",
|
||||
"find-up",
|
||||
"for-in",
|
||||
"for-own",
|
||||
"forever-agent",
|
||||
"form-data",
|
||||
"fragment-cache",
|
||||
"fs.realpath",
|
||||
"get-caller-file",
|
||||
"get-value",
|
||||
"glob",
|
||||
"glob-base",
|
||||
|
@ -104,6 +111,7 @@ filegroup(
|
|||
"https-proxy-agent",
|
||||
"inflight",
|
||||
"inherits",
|
||||
"is-arrayish",
|
||||
"is-accessor-descriptor",
|
||||
"is-binary-path",
|
||||
"is-buffer",
|
||||
|
@ -132,6 +140,7 @@ filegroup(
|
|||
"json-stringify-safe",
|
||||
"jsprim",
|
||||
"kind-of",
|
||||
"locate-path",
|
||||
"long",
|
||||
"lru-cache",
|
||||
"magic-string",
|
||||
|
@ -155,12 +164,19 @@ filegroup(
|
|||
"once",
|
||||
"optimist",
|
||||
"options",
|
||||
"os-locale",
|
||||
"os-tmpdir",
|
||||
"p-limit",
|
||||
"p-locate",
|
||||
"p-try",
|
||||
"parse-glob",
|
||||
"parse-json",
|
||||
"pascalcase",
|
||||
"path-dirname",
|
||||
"path-exists",
|
||||
"path-is-absolute",
|
||||
"performance-now",
|
||||
"pify",
|
||||
"posix-character-classes",
|
||||
"preserve",
|
||||
"process-nextick-args",
|
||||
|
@ -168,6 +184,7 @@ filegroup(
|
|||
"protractor",
|
||||
"qs",
|
||||
"randomatic",
|
||||
"read-pkg-up",
|
||||
"readable-stream",
|
||||
"readdirp",
|
||||
"reflect-metadata",
|
||||
|
@ -177,6 +194,8 @@ filegroup(
|
|||
"repeat-element",
|
||||
"repeat-string",
|
||||
"request",
|
||||
"require-directory",
|
||||
"require-main-filename",
|
||||
"ret",
|
||||
"rimraf",
|
||||
"safe-buffer",
|
||||
|
@ -184,6 +203,7 @@ filegroup(
|
|||
"safer-buffer",
|
||||
"sax",
|
||||
"semver",
|
||||
"set-blocking",
|
||||
"set-immediate-shim",
|
||||
"set-value",
|
||||
"shelljs",
|
||||
|
@ -200,6 +220,7 @@ filegroup(
|
|||
"sshpk",
|
||||
"static-extend",
|
||||
"stringstream",
|
||||
"strip-bom",
|
||||
"tmp",
|
||||
"to-object-path",
|
||||
"to-regex",
|
||||
|
@ -226,6 +247,9 @@ filegroup(
|
|||
"xhr2",
|
||||
"xml2js",
|
||||
"xmlbuilder",
|
||||
"y18n",
|
||||
"yargs",
|
||||
"yargs-parser",
|
||||
"zone.js",
|
||||
"@angular-devkit/core",
|
||||
"@angular-devkit/schematics",
|
||||
|
|
|
@ -143,6 +143,10 @@
|
|||
version "0.19.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-0.19.32.tgz#e9204c4cdbc8e275d645c00e6150e68fc5615a24"
|
||||
|
||||
"@types/yargs@^11.1.1":
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-11.1.1.tgz#2e724257167fd6b615dbe4e54301e65fe597433f"
|
||||
|
||||
"@webcomponents/custom-elements@^1.0.4":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.1.2.tgz#041e4c20df35245f4d160b50d044b8cff192962c"
|
||||
|
@ -1905,6 +1909,10 @@ depd@~1.1.0, depd@~1.1.2:
|
|||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
|
||||
dependency-graph@^0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.7.2.tgz#91db9de6eb72699209d88aea4c1fd5221cac1c49"
|
||||
|
||||
deprecated@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19"
|
||||
|
|
Loading…
Reference in New Issue