From 9b1bb370a3a39755a8262041d2285e0d52c14e09 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 9 Aug 2018 15:59:10 +0100 Subject: [PATCH] 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 --- integration/ngcc/.gitignore | 1 + integration/ngcc/test.sh | 9 +- package.json | 2 + packages/compiler-cli/package.json | 6 +- .../compiler-cli/src/ngcc/canonical-path.d.ts | 2 +- packages/compiler-cli/src/ngcc/src/main.ts | 118 ++++----- .../src/ngcc/src/packages/build_marker.ts | 41 +++ .../src/ngcc/src/packages/dependency_host.ts | 140 ++++++++++ .../ngcc/src/packages/dependency_resolver.ts | 129 ++++++++++ .../src/ngcc/src/packages/entry_point.ts | 97 +++++++ .../ngcc/src/packages/entry_point_finder.ts | 111 ++++++++ .../transformer.ts} | 118 ++++----- .../ngcc/test/packages/build_marker_spec.ts | 154 +++++++++++ .../test/packages/dependency_host_spec.ts | 224 ++++++++++++++++ .../test/packages/dependency_resolver_spec.ts | 108 ++++++++ .../test/packages/entry_point_finder_spec.ts | 165 ++++++++++++ .../ngcc/test/packages/entry_point_spec.ts | 97 +++++++ .../src/ngcc/test/transform/utils_spec.ts | 240 ------------------ packages/compiler-cli/test/ngcc/ngcc_spec.ts | 36 ++- tools/ng_setup_workspace.bzl | 24 ++ yarn.lock | 8 + 21 files changed, 1441 insertions(+), 389 deletions(-) create mode 100644 integration/ngcc/.gitignore create mode 100644 packages/compiler-cli/src/ngcc/src/packages/build_marker.ts create mode 100644 packages/compiler-cli/src/ngcc/src/packages/dependency_host.ts create mode 100644 packages/compiler-cli/src/ngcc/src/packages/dependency_resolver.ts create mode 100644 packages/compiler-cli/src/ngcc/src/packages/entry_point.ts create mode 100644 packages/compiler-cli/src/ngcc/src/packages/entry_point_finder.ts rename packages/compiler-cli/src/ngcc/src/{transform/package_transformer.ts => packages/transformer.ts} (65%) create mode 100644 packages/compiler-cli/src/ngcc/test/packages/build_marker_spec.ts create mode 100644 packages/compiler-cli/src/ngcc/test/packages/dependency_host_spec.ts create mode 100644 packages/compiler-cli/src/ngcc/test/packages/dependency_resolver_spec.ts create mode 100644 packages/compiler-cli/src/ngcc/test/packages/entry_point_finder_spec.ts create mode 100644 packages/compiler-cli/src/ngcc/test/packages/entry_point_spec.ts delete mode 100644 packages/compiler-cli/src/ngcc/test/transform/utils_spec.ts diff --git a/integration/ngcc/.gitignore b/integration/ngcc/.gitignore new file mode 100644 index 0000000000..8ee01d321b --- /dev/null +++ b/integration/ngcc/.gitignore @@ -0,0 +1 @@ +yarn.lock diff --git a/integration/ngcc/test.sh b/integration/ngcc/test.sh index 4efd1e7d15..8b3b1980e5 100755 --- a/integration/ngcc/test.sh +++ b/integration/ngcc/test.sh @@ -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 diff --git a/package.json b/package.json index fcfa64c244..287785f3a7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/compiler-cli/package.json b/packages/compiler-cli/package.json index edb982f027..93a3a281d4 100644 --- a/packages/compiler-cli/package.json +++ b/packages/compiler-cli/package.json @@ -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", diff --git a/packages/compiler-cli/src/ngcc/canonical-path.d.ts b/packages/compiler-cli/src/ngcc/canonical-path.d.ts index 79b8596b91..c8c8b88610 100644 --- a/packages/compiler-cli/src/ngcc/canonical-path.d.ts +++ b/packages/compiler-cli/src/ngcc/canonical-path.d.ts @@ -11,4 +11,4 @@ declare module 'canonical-path' { export var delimiter: string; export function parse(p: string): ParsedPath; export function format(pP: ParsedPath): string; -} \ No newline at end of file +} diff --git a/packages/compiler-cli/src/ngcc/src/main.ts b/packages/compiler-cli/src/ngcc/src/main.ts index a598ec5953..851589d7f0 100644 --- a/packages/compiler-cli/src/ngcc/src/main.ts +++ b/packages/compiler-cli/src/ngcc/src/main.ts @@ -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); - }); -} diff --git a/packages/compiler-cli/src/ngcc/src/packages/build_marker.ts b/packages/compiler-cli/src/ngcc/src/packages/build_marker.ts new file mode 100644 index 0000000000..986b296a26 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/packages/build_marker.ts @@ -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'); +} diff --git a/packages/compiler-cli/src/ngcc/src/packages/dependency_host.ts b/packages/compiler-cli/src/ngcc/src/packages/dependency_host.ts new file mode 100644 index 0000000000..f6d7bbd38b --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/packages/dependency_host.ts @@ -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, missing: Set, + internal: Set = 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); + } +} diff --git a/packages/compiler-cli/src/ngcc/src/packages/dependency_resolver.ts b/packages/compiler-cli/src/ngcc/src/packages/dependency_resolver.ts new file mode 100644 index 0000000000..b825553c33 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/packages/dependency_resolver.ts @@ -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(); + + // 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(); + const missing = new Set(); + 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); + }); + } + } +} diff --git a/packages/compiler-cli/src/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/src/ngcc/src/packages/entry_point.ts new file mode 100644 index 0000000000..78465318c9 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/packages/entry_point.ts @@ -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; +} diff --git a/packages/compiler-cli/src/ngcc/src/packages/entry_point_finder.ts b/packages/compiler-cli/src/ngcc/src/packages/entry_point_finder.ts new file mode 100644 index 0000000000..32668ee2f6 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/packages/entry_point_finder.ts @@ -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); + }); +} diff --git a/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts similarity index 65% rename from packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts rename to packages/compiler-cli/src/ngcc/src/packages/transformer.ts index 8176355917..68e1ebb73e 100644 --- a/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts +++ b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts @@ -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[] { diff --git a/packages/compiler-cli/src/ngcc/test/packages/build_marker_spec.ts b/packages/compiler-cli/src/ngcc/test/packages/build_marker_spec.ts new file mode 100644 index 0000000000..389906940a --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/packages/build_marker_spec.ts @@ -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.'); + }); + }); +}); diff --git a/packages/compiler-cli/src/ngcc/test/packages/dependency_host_spec.ts b/packages/compiler-cli/src/ngcc/test/packages/dependency_host_spec.ts new file mode 100644 index 0000000000..4586aacc53 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/packages/dependency_host_spec.ts @@ -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(); } +}); diff --git a/packages/compiler-cli/src/ngcc/test/packages/dependency_resolver_spec.ts b/packages/compiler-cli/src/ngcc/test/packages/dependency_resolver_spec.ts new file mode 100644 index 0000000000..f534f36da5 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/packages/dependency_resolver_spec.ts @@ -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, missing: Set) => { + dependencies[entryPoint].resolved.forEach(dep => resolved.add(dep)); + dependencies[entryPoint].missing.forEach(dep => missing.add(dep)); + }; + } + }); +}); diff --git a/packages/compiler-cli/src/ngcc/test/packages/entry_point_finder_spec.ts b/packages/compiler-cli/src/ngcc/test/packages/entry_point_finder_spec.ts new file mode 100644 index 0000000000..6ceeae840a --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/packages/entry_point_finder_spec.ts @@ -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); +} diff --git a/packages/compiler-cli/src/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/src/ngcc/test/packages/entry_point_spec.ts new file mode 100644 index 0000000000..e6d8cb9068 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/packages/entry_point_spec.ts @@ -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); +} diff --git a/packages/compiler-cli/src/ngcc/test/transform/utils_spec.ts b/packages/compiler-cli/src/ngcc/test/transform/utils_spec.ts deleted file mode 100644 index f0d2d20a3e..0000000000 --- a/packages/compiler-cli/src/ngcc/test/transform/utils_spec.ts +++ /dev/null @@ -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.'); - }); - }); -}); diff --git a/packages/compiler-cli/test/ngcc/ngcc_spec.ts b/packages/compiler-cli/test/ngcc/ngcc_spec.ts index d8e80225c8..1f4f1c5e44 100644 --- a/packages/compiler-cli/test/ngcc/ngcc_spec.ts +++ b/packages/compiler-cli/test/ngcc/ngcc_spec.ts @@ -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() { @@ -104,4 +102,24 @@ 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; } \ No newline at end of file diff --git a/tools/ng_setup_workspace.bzl b/tools/ng_setup_workspace.bzl index dda119c261..f5a2a6628d 100644 --- a/tools/ng_setup_workspace.bzl +++ b/tools/ng_setup_workspace.bzl @@ -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", diff --git a/yarn.lock b/yarn.lock index 28f365726d..d1650e0a2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"