From a9a65f364ebd35972ff7a75ef7072d1a8ecb1959 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:56 +0100 Subject: [PATCH 01/13] refactor(compiler-cli): ngcc - remove unnecessary `sourcePath` parameters The `Transformer` and `Renderer` classes do not actually need a `sourcePath` value as by the time they are doing their work we are only working directly with full absolute paths. --- packages/compiler-cli/ngcc/src/main.ts | 2 +- packages/compiler-cli/ngcc/src/packages/transformer.ts | 6 +++--- packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts | 6 ++---- packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts | 6 ++---- packages/compiler-cli/ngcc/src/rendering/renderer.ts | 2 +- .../ngcc/test/rendering/esm2015_renderer_spec.ts | 3 +-- .../compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts | 3 +-- packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts | 2 +- 8 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 35cec79ba763..24967430dae7 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -73,7 +73,7 @@ export function mainNgcc({basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, compileAllFormats = true, createNewEntryPointFormats = false, logger = new ConsoleLogger(LogLevel.info)}: NgccOptions): void { - const transformer = new Transformer(logger, basePath); + const transformer = new Transformer(logger); const host = new DependencyHost(); const resolver = new DependencyResolver(logger, host); const finder = new EntryPointFinder(logger, resolver); diff --git a/packages/compiler-cli/ngcc/src/packages/transformer.ts b/packages/compiler-cli/ngcc/src/packages/transformer.ts index f4a9e7675323..3ca19262bff6 100644 --- a/packages/compiler-cli/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/ngcc/src/packages/transformer.ts @@ -46,7 +46,7 @@ import {EntryPointBundle} from './entry_point_bundle'; * - Some formats may contain multiple "modules" in a single file. */ export class Transformer { - constructor(private logger: Logger, private sourcePath: string) {} + constructor(private logger: Logger) {} /** * Transform the source (and typings) files of a bundle. @@ -85,9 +85,9 @@ export class Transformer { getRenderer(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): Renderer { switch (bundle.format) { case 'esm2015': - return new EsmRenderer(this.logger, host, isCore, bundle, this.sourcePath); + return new EsmRenderer(this.logger, host, isCore, bundle); case 'esm5': - return new Esm5Renderer(this.logger, host, isCore, bundle, this.sourcePath); + return new Esm5Renderer(this.logger, host, isCore, bundle); default: throw new Error(`Renderer for "${bundle.format}" not yet implemented.`); } diff --git a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts index 9268fef45b44..7158b250a15a 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts @@ -15,10 +15,8 @@ import {EntryPointBundle} from '../packages/entry_point_bundle'; import {Logger} from '../logging/logger'; export class Esm5Renderer extends EsmRenderer { - constructor( - logger: Logger, host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle, - sourcePath: string) { - super(logger, host, isCore, bundle, sourcePath); + constructor(logger: Logger, host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle) { + super(logger, host, isCore, bundle); } /** diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts index 8e6b4f2f1051..04a3b928f969 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts @@ -17,10 +17,8 @@ import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {Logger} from '../logging/logger'; export class EsmRenderer extends Renderer { - constructor( - logger: Logger, host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle, - sourcePath: string) { - super(logger, host, isCore, bundle, sourcePath); + constructor(logger: Logger, host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle) { + super(logger, host, isCore, bundle); } /** diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index 39d0a9caa115..b6016a2ce6d5 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -82,7 +82,7 @@ export const RedundantDecoratorMap = Map; export abstract class Renderer { constructor( protected logger: Logger, protected host: NgccReflectionHost, protected isCore: boolean, - protected bundle: EntryPointBundle, protected sourcePath: string) {} + protected bundle: EntryPointBundle) {} renderProgram( decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses, diff --git a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts index d36d5838f691..c28d25edb0f2 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -19,7 +19,6 @@ import {MockLogger} from '../helpers/mock_logger'; function setup(file: {name: string, contents: string}) { const logger = new MockLogger(); - const dir = dirname(file.name); const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, [file]) !; const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(logger, false, typeChecker); @@ -30,7 +29,7 @@ function setup(file: {name: string, contents: string}) { referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new EsmRenderer(logger, host, false, bundle, dir); + const renderer = new EsmRenderer(logger, host, false, bundle); return { host, program: bundle.src.program, diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts index a7d7c17614b4..8e6e058633e3 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts @@ -19,7 +19,6 @@ import {MockLogger} from '../helpers/mock_logger'; function setup(file: {name: string, contents: string}) { const logger = new MockLogger(); - const dir = dirname(file.name); const bundle = makeTestEntryPointBundle('module', 'esm5', false, [file]); const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm5ReflectionHost(logger, false, typeChecker); @@ -30,7 +29,7 @@ function setup(file: {name: string, contents: string}) { referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new Esm5Renderer(logger, host, false, bundle, dir); + const renderer = new Esm5Renderer(logger, host, false, bundle); return { host, program: bundle.src.program, diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index cea122f6af3a..1e9e3bd3872b 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -24,7 +24,7 @@ import {MockLogger} from '../helpers/mock_logger'; class TestRenderer extends Renderer { constructor( logger: Logger, host: Esm2015ReflectionHost, isCore: boolean, bundle: EntryPointBundle) { - super(logger, host, isCore, bundle, '/src'); + super(logger, host, isCore, bundle); } addImports( output: MagicString, imports: {specifier: string, qualifier: string}[], sf: ts.SourceFile) { From 4780fb27eeaee1247eac373329d05c07fc9a8b70 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:56 +0100 Subject: [PATCH 02/13] refactor(ivy): ngcc - tidy up `DependencyResolver` helper method This method was poorly named for what it does, and did not have a return type. --- .../ngcc/src/packages/dependency_resolver.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts b/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts index 59737ed322f9..ef37a548a31f 100644 --- a/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts +++ b/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {resolve} from 'canonical-path'; import {DepGraph} from 'dependency-graph'; +import {resolve} from 'path'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {Logger} from '../logging/logger'; @@ -48,6 +48,11 @@ export interface IgnoredDependency { dependencyPath: string; } +export interface DependencyDiagnostics { + invalidEntryPoints: InvalidEntryPoint[]; + ignoredDependencies: IgnoredDependency[]; +} + /** * A list of entry-points, sorted by their dependencies. * @@ -57,11 +62,7 @@ export interface IgnoredDependency { * 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[]; -} +export interface SortedEntryPointsInfo extends DependencyDiagnostics { entryPoints: EntryPoint[]; } /** * A class that resolves dependencies between entry-points. @@ -77,7 +78,8 @@ export class DependencyResolver { */ sortEntryPointsByDependency(entryPoints: EntryPoint[], target?: EntryPoint): SortedEntryPointsInfo { - const {invalidEntryPoints, ignoredDependencies, graph} = this.createDependencyInfo(entryPoints); + const {invalidEntryPoints, ignoredDependencies, graph} = + this.computeDependencyGraph(entryPoints); let sortedEntryPointNodes: string[]; if (target) { @@ -100,7 +102,7 @@ export class DependencyResolver { * The graph only holds entry-points that ngcc cares about and whose dependencies * (direct and transitive) all exist. */ - private createDependencyInfo(entryPoints: EntryPoint[]) { + private computeDependencyGraph(entryPoints: EntryPoint[]): DependencyGraph { const invalidEntryPoints: InvalidEntryPoint[] = []; const ignoredDependencies: IgnoredDependency[] = []; const graph = new DepGraph(); @@ -167,3 +169,7 @@ function getEntryPointPath(entryPoint: EntryPoint): AbsoluteFsPath { } throw new Error(`There is no format with import statements in '${entryPoint.path}' entry-point.`); } + +interface DependencyGraph extends DependencyDiagnostics { + graph: DepGraph; +} From 50731b8442964859099744bce07fced7f2938e7e Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:56 +0100 Subject: [PATCH 03/13] test(ivy): ngcc - check private dependency in integration test The test now attempts to compile an entry-point (@angular/common/http/testing) that has a transient "private" dependency. A private dependency is one that is only visible by looking at the compiled JS code, rather than the generated TS typings files. This proves that we can't rely on typings files alone for computing the dependencies between entry-points. --- .../ngcc/test/integration/ngcc_spec.ts | 76 ++++--------------- 1 file changed, 16 insertions(+), 60 deletions(-) diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 6b72e3a47d89..6500dc9fc979 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -10,7 +10,6 @@ import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; import {existsSync, readFileSync, readdirSync, statSync, writeFileSync} from 'fs'; import * as mockFs from 'mock-fs'; import {join} from 'path'; -const Module = require('module'); import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../../../test/runfile_helpers'; import {mainNgcc} from '../../src/main'; @@ -40,29 +39,7 @@ describe('ngcc main()', () => { describe('with targetEntryPointPath', () => { it('should only compile the given package entry-point (and its dependencies).', () => { - mainNgcc({basePath: '/node_modules', targetEntryPointPath: '@angular/common/http'}); - - expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({ - module: '0.0.0-PLACEHOLDER', - es2015: '0.0.0-PLACEHOLDER', - esm5: '0.0.0-PLACEHOLDER', - esm2015: '0.0.0-PLACEHOLDER', - fesm5: '0.0.0-PLACEHOLDER', - fesm2015: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - // * `common` is a dependency of `common/http`, so is compiled. - expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({ - module: '0.0.0-PLACEHOLDER', - es2015: '0.0.0-PLACEHOLDER', - esm5: '0.0.0-PLACEHOLDER', - esm2015: '0.0.0-PLACEHOLDER', - fesm5: '0.0.0-PLACEHOLDER', - fesm2015: '0.0.0-PLACEHOLDER', - typings: '0.0.0-PLACEHOLDER', - }); - // * `core` is a dependency of `common`, so is compiled. - expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + const STANDARD_MARKERS = { module: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER', esm5: '0.0.0-PLACEHOLDER', @@ -70,9 +47,19 @@ describe('ngcc main()', () => { fesm5: '0.0.0-PLACEHOLDER', fesm2015: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', - }); - - // * `common/testing` is not a dependency of `common/http` so is not compiled. + }; + + mainNgcc({basePath: '/node_modules', targetEntryPointPath: '@angular/common/http/testing'}); + expect(loadPackage('@angular/common/http/testing').__processed_by_ivy_ngcc__) + .toEqual(STANDARD_MARKERS); + // * `common/http` is a dependency of `common/http/testing`, so is compiled. + expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__) + .toEqual(STANDARD_MARKERS); + // * `core` is a dependency of `common/http`, so is compiled. + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS); + // * `common` is a private (only in .js not .d.ts) dependency so is compiled. + expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS); + // * `common/testing` is not a dependency so is not compiled. expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toBeUndefined(); }); @@ -332,13 +319,13 @@ function createMockFileSystem() { '/node_modules/tslib': loadDirectory(resolveNpmTreeArtifact('tslib', 'tslib.js')), '/node_modules/test-package': { 'package.json': '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}', + // no metadata.json file so not compiled by Angular. 'index.js': 'import {AppModule} from "@angular/common"; export class MyApp extends AppModule;', - 'index.d.s': + 'index.d.ts': 'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;', } }); - spyOn(Module, '_resolveFilename').and.callFake(mockResolve); } function restoreRealFileSystem() { @@ -380,37 +367,6 @@ interface Directory { [pathSegment: string]: string|Directory; } -/** - * A mock implementation of the node.js Module._resolveFilename function, - * which we are spying on to support mocking out the file-system in these tests. - * - * @param request the path to a module that needs resolving. - */ -function mockResolve(request: string): string|null { - if (existsSync(request)) { - const stat = statSync(request); - if (stat.isFile()) { - return request; - } else if (stat.isDirectory()) { - const pIndex = mockResolve(request + '/index'); - if (pIndex && existsSync(pIndex)) { - return pIndex; - } - } - } - for (const ext of ['.js', '.d.ts']) { - if (existsSync(request + ext)) { - return request + ext; - } - } - if (request.indexOf('/node_modules') === 0) { - // We already tried adding node_modules so give up. - return null; - } else { - return mockResolve(join('/node_modules', request)); - } -} - function loadPackage(packageName: string): EntryPointPackageJson { return JSON.parse(readFileSync(`/node_modules/${packageName}/package.json`, 'utf8')); } From 7a74d3f1bd2399bd13cc2b3d6b791d905ecbb8eb Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:56 +0100 Subject: [PATCH 04/13] refactor(compiler-cli): ngcc - track non-Angular entry-points Previously we completely ignored entry-points that had not been compiled with Angular, since we do not need to compile them with ngcc. But this makes it difficult to reason about dependencies between entry-points that were compiled with Angular and those that were not. Now we do track these non-Angular compiled entry-points but they are marked as `compiledByAngular: false`. --- .../ngcc/src/packages/dependency_resolver.ts | 16 +++-- .../ngcc/src/packages/entry_point.ts | 6 +- .../test/packages/dependency_resolver_spec.ts | 58 +++++++++++++------ .../ngcc/test/packages/entry_point_spec.ts | 27 +++++---- 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts b/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts index ef37a548a31f..15e1c3d3f841 100644 --- a/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts +++ b/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts @@ -83,8 +83,12 @@ export class DependencyResolver { let sortedEntryPointNodes: string[]; if (target) { - sortedEntryPointNodes = graph.dependenciesOf(target.path); - sortedEntryPointNodes.push(target.path); + if (target.compiledByAngular) { + sortedEntryPointNodes = graph.dependenciesOf(target.path); + sortedEntryPointNodes.push(target.path); + } else { + sortedEntryPointNodes = []; + } } else { sortedEntryPointNodes = graph.overallOrder(); } @@ -107,11 +111,13 @@ export class DependencyResolver { const ignoredDependencies: IgnoredDependency[] = []; const graph = new DepGraph(); - // Add the entry points to the graph as nodes - entryPoints.forEach(entryPoint => graph.addNode(entryPoint.path, entryPoint)); + const angularEntryPoints = entryPoints.filter(entryPoint => entryPoint.compiledByAngular); + + // Add the Angular compiled entry points to the graph as nodes + angularEntryPoints.forEach(entryPoint => graph.addNode(entryPoint.path, entryPoint)); // Now add the dependencies between them - entryPoints.forEach(entryPoint => { + angularEntryPoints.forEach(entryPoint => { const entryPointPath = getEntryPointPath(entryPoint); const {dependencies, missing, deepImports} = this.host.computeDependencies(entryPointPath); diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/ngcc/src/packages/entry_point.ts index a51ce1f25f9d..5f1d92fe26d3 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point.ts @@ -33,6 +33,8 @@ export interface EntryPoint { path: AbsoluteFsPath; /** The path to a typings (.d.ts) file for this entry-point. */ typings: AbsoluteFsPath; + /** Is this EntryPoint compiled with the Angular View Engine compiler? */ + compiledByAngular: boolean; } interface PackageJsonFormatProperties { @@ -89,9 +91,6 @@ export function getEntryPointInfo( // Also there must exist a `metadata.json` file next to the typings entry-point. const metadataPath = path.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json'); - if (!fs.existsSync(metadataPath)) { - return null; - } const entryPointInfo: EntryPoint = { name: entryPointPackageJson.name, @@ -99,6 +98,7 @@ export function getEntryPointInfo( package: packagePath, path: entryPointPath, typings: AbsoluteFsPath.from(path.resolve(entryPointPath, typings)), + compiledByAngular: fs.existsSync(metadataPath), }; return entryPointInfo; diff --git a/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts index 60c8f16d303d..1618ab8f0e23 100644 --- a/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts @@ -21,18 +21,38 @@ describe('DependencyResolver', () => { resolver = new DependencyResolver(new MockLogger(), host); }); describe('sortEntryPointsByDependency()', () => { - const first = { path: _('/first'), packageJson: {esm5: 'index.ts'} } as EntryPoint; - const second = { path: _('/second'), packageJson: {esm2015: 'sub/index.ts'} } as EntryPoint; - const third = { path: _('/third'), packageJson: {esm5: 'index.ts'} } as EntryPoint; - const fourth = { path: _('/fourth'), packageJson: {esm2015: 'sub2/index.ts'} } as EntryPoint; - const fifth = { path: _('/fifth'), packageJson: {esm5: 'index.ts'} } as EntryPoint; + const first = { + path: _('/first'), + packageJson: {esm5: './index.js'}, + compiledByAngular: true + } as EntryPoint; + const second = { + path: _('/second'), + packageJson: {esm2015: './sub/index.js'}, + compiledByAngular: true + } as EntryPoint; + const third = { + path: _('/third'), + packageJson: {fesm5: './index.js'}, + compiledByAngular: true + } as EntryPoint; + const fourth = { + path: _('/fourth'), + packageJson: {fesm2015: './sub2/index.js'}, + compiledByAngular: true + } as EntryPoint; + const fifth = { + path: _('/fifth'), + packageJson: {module: './index.js'}, + compiledByAngular: true + } as EntryPoint; const dependencies = { - [_('/first/index.ts')]: {resolved: [second.path, third.path, '/ignored-1'], missing: []}, - [_('/second/sub/index.ts')]: {resolved: [third.path, fifth.path], missing: []}, - [_('/third/index.ts')]: {resolved: [fourth.path, '/ignored-2'], missing: []}, - [_('/fourth/sub2/index.ts')]: {resolved: [fifth.path], missing: []}, - [_('/fifth/index.ts')]: {resolved: [], missing: []}, + [_('/first/index.js')]: {resolved: [second.path, third.path, '/ignored-1'], missing: []}, + [_('/second/sub/index.js')]: {resolved: [third.path, fifth.path], missing: []}, + [_('/third/index.js')]: {resolved: [fourth.path, '/ignored-2'], missing: []}, + [_('/fourth/sub2/index.js')]: {resolved: [fifth.path], missing: []}, + [_('/fifth/index.js')]: {resolved: [], missing: []}, }; it('should order the entry points by their dependency on each other', () => { @@ -43,8 +63,8 @@ describe('DependencyResolver', () => { it('should remove entry-points that have missing direct dependencies', () => { spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({ - [_('/first/index.ts')]: {resolved: [], missing: ['/missing']}, - [_('/second/sub/index.ts')]: {resolved: [], missing: []}, + [_('/first/index.js')]: {resolved: [], missing: ['/missing']}, + [_('/second/sub/index.js')]: {resolved: [], missing: []}, })); const result = resolver.sortEntryPointsByDependency([first, second]); expect(result.entryPoints).toEqual([second]); @@ -55,9 +75,9 @@ describe('DependencyResolver', () => { it('should remove entry points that depended upon an invalid entry-point', () => { spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({ - [_('/first/index.ts')]: {resolved: [second.path], missing: []}, - [_('/second/sub/index.ts')]: {resolved: [], missing: ['/missing']}, - [_('/third/index.ts')]: {resolved: [], missing: []}, + [_('/first/index.js')]: {resolved: [second.path], missing: []}, + [_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']}, + [_('/third/index.js')]: {resolved: [], missing: []}, })); // Note that we will process `first` before `second`, which has the missing dependency. const result = resolver.sortEntryPointsByDependency([first, second, third]); @@ -70,9 +90,9 @@ describe('DependencyResolver', () => { it('should remove entry points that will depend upon an invalid entry-point', () => { spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({ - [_('/first/index.ts')]: {resolved: [second.path], missing: []}, - [_('/second/sub/index.ts')]: {resolved: [], missing: ['/missing']}, - [_('/third/index.ts')]: {resolved: [], missing: []}, + [_('/first/index.js')]: {resolved: [second.path], missing: []}, + [_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']}, + [_('/third/index.js')]: {resolved: [], missing: []}, })); // Note that we will process `first` after `second`, which has the missing dependency. const result = resolver.sortEntryPointsByDependency([second, first, third]); @@ -85,7 +105,7 @@ describe('DependencyResolver', () => { it('should error if the entry point does not have either the esm5 nor esm2015 formats', () => { expect(() => resolver.sortEntryPointsByDependency([ - { path: '/first', packageJson: {} } as EntryPoint + { path: '/first', packageJson: {}, compiledByAngular: true } as EntryPoint ])).toThrowError(`There is no format with import statements in '/first' entry-point.`); }); diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts index c360d80dfd59..a4c028d32838 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -30,6 +30,7 @@ describe('getEntryPointInfo()', () => { path: _('/some_package/valid_entry_point'), typings: _(`/some_package/valid_entry_point/valid_entry_point.d.ts`), packageJson: loadPackageJson('/some_package/valid_entry_point'), + compiledByAngular: true, }); }); @@ -45,17 +46,19 @@ describe('getEntryPointInfo()', () => { expect(entryPoint).toBe(null); }); - it('should return null if there is no esm2015 nor fesm2015 field in the package.json', () => { - const entryPoint = - getEntryPointInfo(new MockLogger(), 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(new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata.json')); - expect(entryPoint).toBe(null); - }); + it('should return an object with `compiledByAngular` set to false if there is no metadata.json file next to the typing file', + () => { + const entryPoint = + getEntryPointInfo(new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata')); + expect(entryPoint).toEqual({ + name: 'some-package/missing_metadata', + package: SOME_PACKAGE, + path: _('/some_package/missing_metadata'), + typings: _(`/some_package/missing_metadata/missing_metadata.d.ts`), + packageJson: loadPackageJson('/some_package/missing_metadata'), + compiledByAngular: false, + }); + }); it('should work if the typings field is named `types', () => { const entryPoint = getEntryPointInfo( @@ -66,6 +69,7 @@ describe('getEntryPointInfo()', () => { path: _('/some_package/types_rather_than_typings'), typings: _(`/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`), packageJson: loadPackageJson('/some_package/types_rather_than_typings'), + compiledByAngular: true, }); }); @@ -78,6 +82,7 @@ describe('getEntryPointInfo()', () => { path: _('/some_package/material_style'), typings: _(`/some_package/material_style/material_style.d.ts`), packageJson: JSON.parse(readFileSync('/some_package/material_style/package.json', 'utf8')), + compiledByAngular: true, }); }); From d017181e346b9db1d39ffb082c88ad805cdb6f6a Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:56 +0100 Subject: [PATCH 05/13] refactor(ivy): ngcc - tidy up `mainNgcc` --- packages/compiler-cli/ngcc/src/main.ts | 34 ++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 24967430dae7..63c802a24736 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -93,20 +93,8 @@ export function mainNgcc({basePath, targetEntryPointPath, const {entryPoints} = finder.findEntryPoints(AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath); - if (absoluteTargetEntryPointPath && entryPoints.every(entryPoint => { - return entryPoint.path !== absoluteTargetEntryPointPath; - })) { - // If we get here, then the requested entry-point did not contain anything compiled by - // the old Angular compiler. Therefore there is nothing for ngcc to do. - // So mark all formats in this entry-point as processed so that clients of ngcc can avoid - // triggering ngcc for this entry-point in the future. - const packageJsonPath = - AbsoluteFsPath.from(resolve(absoluteTargetEntryPointPath, 'package.json')); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); - propertiesToConsider.forEach(formatProperty => { - if (packageJson[formatProperty]) - markAsProcessed(packageJson, packageJsonPath, formatProperty as EntryPointJsonProperty); - }); + if (absoluteTargetEntryPointPath && entryPoints.length === 0) { + markNonAngularPackageAsProcessed(absoluteTargetEntryPointPath, propertiesToConsider); return; } @@ -116,7 +104,8 @@ export function mainNgcc({basePath, targetEntryPointPath, const compiledFormats = new Set(); const entryPointPackageJson = entryPoint.packageJson; - const entryPointPackageJsonPath = AbsoluteFsPath.from(resolve(entryPoint.path, 'package.json')); + const entryPointPackageJsonPath = + AbsoluteFsPath.fromUnchecked(`${entryPoint.path}/package.json`); const hasProcessedDts = hasBeenProcessed(entryPointPackageJson, 'typings'); @@ -200,3 +189,18 @@ function hasProcessedTargetEntryPoint( // property before the first processed format that was unprocessed. return true; } + +/** + * If we get here, then the requested entry-point did not contain anything compiled by + * the old Angular compiler. Therefore there is nothing for ngcc to do. + * So mark all formats in this entry-point as processed so that clients of ngcc can avoid + * triggering ngcc for this entry-point in the future. + */ +function markNonAngularPackageAsProcessed(path: AbsoluteFsPath, propertiesToConsider: string[]) { + const packageJsonPath = AbsoluteFsPath.from(resolve(path, 'package.json')); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + propertiesToConsider.forEach(formatProperty => { + if (packageJson[formatProperty]) + markAsProcessed(packageJson, packageJsonPath, formatProperty as EntryPointJsonProperty); + }); +} From 4f007c7985d67b5652e9b5a00067be53fe34331f Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:57 +0100 Subject: [PATCH 06/13] refactor(ivy): ngcc - implement new module resolver When working out the dependencies between entry-points ngcc must parse the import statements and then resolve the import path to the actual file. This is complicated because module resolution is not trivial. Previously ngcc used the node.js `require.resolve`, with some hacking to resolve modules. This change refactors the `DependencyHost` to use a new custom `ModuleResolver`, which is optimized for this use case. Moreover, because we are in full control of the resolution, we can support TS `paths` aliases, where not all imports come from `node_modules`. This is the case in some CLI projects where there are compiled libraries that are stored locally in a `dist` folder. See //FW-1210. --- packages/compiler-cli/ngcc/src/main.ts | 4 +- .../ngcc/src/packages/dependency_host.ts | 106 ++----- .../ngcc/src/packages/module_resolver.ts | 280 ++++++++++++++++++ packages/compiler-cli/ngcc/src/utils.ts | 14 + .../test/packages/dependency_host_spec.ts | 263 ++++++++-------- .../test/packages/dependency_resolver_spec.ts | 3 +- .../test/packages/entry_point_finder_spec.ts | 3 +- .../test/packages/module_resolver_spec.ts | 212 +++++++++++++ packages/compiler-cli/ngcc/test/utils_spec.ts | 36 +++ 9 files changed, 691 insertions(+), 230 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/packages/module_resolver.ts create mode 100644 packages/compiler-cli/ngcc/test/packages/module_resolver_spec.ts create mode 100644 packages/compiler-cli/ngcc/test/utils_spec.ts diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 63c802a24736..acee60c7c609 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -19,6 +19,7 @@ import {DependencyResolver} from './packages/dependency_resolver'; import {EntryPointFormat, EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES, getEntryPointFormat} from './packages/entry_point'; import {makeEntryPointBundle} from './packages/entry_point_bundle'; import {EntryPointFinder} from './packages/entry_point_finder'; +import {ModuleResolver} from './packages/module_resolver'; import {Transformer} from './packages/transformer'; import {FileWriter} from './writing/file_writer'; import {InPlaceFileWriter} from './writing/in_place_file_writer'; @@ -74,7 +75,8 @@ export function mainNgcc({basePath, targetEntryPointPath, compileAllFormats = true, createNewEntryPointFormats = false, logger = new ConsoleLogger(LogLevel.info)}: NgccOptions): void { const transformer = new Transformer(logger); - const host = new DependencyHost(); + const moduleResolver = new ModuleResolver(); + const host = new DependencyHost(moduleResolver); const resolver = new DependencyResolver(logger, host); const finder = new EntryPointFinder(logger, resolver); const fileWriter = getFileWriter(createNewEntryPointFormats); diff --git a/packages/compiler-cli/ngcc/src/packages/dependency_host.ts b/packages/compiler-cli/ngcc/src/packages/dependency_host.ts index bd959ed5689f..501d1ccc38cc 100644 --- a/packages/compiler-cli/ngcc/src/packages/dependency_host.ts +++ b/packages/compiler-cli/ngcc/src/packages/dependency_host.ts @@ -6,16 +6,20 @@ * 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'; -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; + +import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; + + /** * Helper functions for computing dependencies. */ export class DependencyHost { + constructor(private moduleResolver: ModuleResolver) {} /** * 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. @@ -24,17 +28,15 @@ export class DependencyHost { * @param missing A set that will have the dependencies that could not be found added to it. * @param deepImports A set that will have the import paths that exist but cannot be mapped to * entry-points, i.e. deep-imports. - * @param internal A set that is used to track internal dependencies to prevent getting stuck in a + * @param alreadySeen A set that is used to track internal dependencies to prevent getting stuck + * in a * circular dependency loop. */ computeDependencies( from: AbsoluteFsPath, dependencies: Set = new Set(), - missing: Set = new Set(), deepImports: Set = new Set(), - internal: Set = new Set()): { - dependencies: Set, - missing: Set, - deepImports: Set - } { + missing: Set = new Set(), deepImports: Set = new Set(), + alreadySeen: Set = new Set()): + {dependencies: Set, missing: Set, deepImports: Set} { const fromContents = fs.readFileSync(from, 'utf8'); if (!this.hasImportOrReexportStatements(fromContents)) { return {dependencies, missing, deepImports}; @@ -49,86 +51,30 @@ export class DependencyHost { // 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: PathSegment) => { - 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, dependencies, missing, deepImports, internal); - } - } else { - const resolvedEntryPoint = this.tryResolveEntryPoint(from, importPath); - if (resolvedEntryPoint !== null) { - dependencies.add(resolvedEntryPoint); + .forEach(importPath => { + const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, from); + if (resolvedModule) { + if (resolvedModule instanceof ResolvedRelativeModule) { + const internalDependency = resolvedModule.modulePath; + if (!alreadySeen.has(internalDependency)) { + alreadySeen.add(internalDependency); + this.computeDependencies( + internalDependency, dependencies, missing, deepImports, alreadySeen); + } } else { - // If the import could not be resolved as entry point, it either does not exist - // at all or is a deep import. - const deeplyImportedFile = this.tryResolve(from, importPath); - if (deeplyImportedFile !== null) { - deepImports.add(importPath); + if (resolvedModule instanceof ResolvedDeepImport) { + deepImports.add(resolvedModule.importPath); } else { - missing.add(importPath); + dependencies.add(resolvedModule.entryPointPath); } } + } else { + missing.add(importPath); } }); return {dependencies, missing, deepImports}; } - /** - * 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: AbsoluteFsPath, to: PathSegment): AbsoluteFsPath { - const fromDirectory = path.dirname(from); - // `fromDirectory` is absolute so we don't need to worry about telling `require.resolve` - // about it by adding it to a `paths` parameter - unlike `tryResolve` below. - return AbsoluteFsPath.from(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. - */ - tryResolveEntryPoint(from: AbsoluteFsPath, to: PathSegment): AbsoluteFsPath|null { - const entryPoint = this.tryResolve(from, `${to}/package.json` as PathSegment); - return entryPoint && AbsoluteFsPath.from(path.dirname(entryPoint)); - } - - /** - * 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: AbsoluteFsPath, to: PathSegment): AbsoluteFsPath|null { - try { - return AbsoluteFsPath.from(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. diff --git a/packages/compiler-cli/ngcc/src/packages/module_resolver.ts b/packages/compiler-cli/ngcc/src/packages/module_resolver.ts new file mode 100644 index 000000000000..1f3e4e704b13 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/packages/module_resolver.ts @@ -0,0 +1,280 @@ +/** + * @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 fs from 'fs'; + +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {PathMappings, isRelativePath} from '../utils'; + + +/** + * This is a very cut-down implementation of the TypeScript module resolution strategy. + * + * It is specific to the needs of ngcc and is not intended to be a drop-in replacement + * for the TS module resolver. It is used to compute the dependencies between entry-points + * that may be compiled by ngcc. + * + * The algorithm only finds `.js` files for internal/relative imports and paths to + * the folder containing the `package.json` of the entry-point for external imports. + * + * It can cope with nested `node_modules` folders and also supports `paths`/`baseUrl` + * configuration properties, as provided in a `ts.CompilerOptions` object. + */ +export class ModuleResolver { + private pathMappings: ProcessedPathMapping[]; + + constructor(pathMappings?: PathMappings, private relativeExtensions = ['.js', '/index.js']) { + this.pathMappings = pathMappings ? this.processPathMappings(pathMappings) : []; + } + + /** + * Resolve an absolute path for the `moduleName` imported into a file at `fromPath`. + * @param moduleName The name of the import to resolve. + * @param fromPath The path to the file containing the import. + * @returns A path to the resolved module or null if missing. + * Specifically: + * * the absolute path to the package.json of an external module + * * a JavaScript file of an internal module + * * null if none exists. + */ + resolveModuleImport(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { + if (isRelativePath(moduleName)) { + return this.resolveAsRelativePath(moduleName, fromPath); + } else { + return this.pathMappings.length && this.resolveByPathMappings(moduleName, fromPath) || + this.resolveAsEntryPoint(moduleName, fromPath); + } + } + + /** + * Convert the `pathMappings` into a collection of `PathMapper` functions. + */ + private processPathMappings(pathMappings: PathMappings): ProcessedPathMapping[] { + const baseUrl = AbsoluteFsPath.from(pathMappings.baseUrl); + return Object.keys(pathMappings.paths).map(pathPattern => { + const matcher = splitOnStar(pathPattern); + const templates = pathMappings.paths[pathPattern].map(splitOnStar); + return {matcher, templates, baseUrl}; + }); + } + + /** + * Try to resolve a module name, as a relative path, from the `fromPath`. + * + * As it is relative, it only looks for files that end in one of the `relativeExtensions`. + * For example: `${moduleName}.js` or `${moduleName}/index.js`. + * If neither of these files exist then the method returns `null`. + */ + private resolveAsRelativePath(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { + const resolvedPath = this.resolvePath( + AbsoluteFsPath.resolve(AbsoluteFsPath.dirname(fromPath), moduleName), + this.relativeExtensions); + return resolvedPath && new ResolvedRelativeModule(resolvedPath); + } + + /** + * Try to resolve the `moduleName`, by applying the computed `pathMappings` and + * then trying to resolve the mapped path as a relative or external import. + * + * Whether the mapped path is relative is defined as it being "below the `fromPath`" and not + * containing `node_modules`. + * + * If the mapped path is not relative but does not resolve to an external entry-point, then we + * check whether it would have resolved to a relative path, in which case it is marked as a + * "deep-import". + */ + private resolveByPathMappings(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { + const mappedPaths = this.findMappedPaths(moduleName); + if (mappedPaths.length > 0) { + const packagePath = this.findPackagePath(fromPath); + if (packagePath !== null) { + for (const mappedPath of mappedPaths) { + const isRelative = + mappedPath.startsWith(packagePath) && !mappedPath.includes('node_modules'); + if (isRelative) { + return this.resolveAsRelativePath(mappedPath, fromPath); + } else if (this.isEntryPoint(mappedPath)) { + return new ResolvedExternalModule(mappedPath); + } else if (this.resolveAsRelativePath(mappedPath, fromPath)) { + return new ResolvedDeepImport(mappedPath); + } + } + } + } + return null; + } + + /** + * Try to resolve the `moduleName` as an external entry-point by searching the `node_modules` + * folders up the tree for a matching `.../node_modules/${moduleName}`. + * + * If a folder is found but the path does not contain a `package.json` then it is marked as a + * "deep-import". + */ + private resolveAsEntryPoint(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { + let folder = fromPath; + while (folder !== '/') { + folder = AbsoluteFsPath.dirname(folder); + if (folder.endsWith('node_modules')) { + // Skip up if the folder already ends in node_modules + folder = AbsoluteFsPath.dirname(folder); + } + const modulePath = AbsoluteFsPath.resolve(folder, 'node_modules', moduleName); + if (this.isEntryPoint(modulePath)) { + return new ResolvedExternalModule(modulePath); + } else if (this.resolveAsRelativePath(modulePath, fromPath)) { + return new ResolvedDeepImport(modulePath); + } + } + return null; + } + + /** + * Attempt to resolve a `path` to a file by appending the provided `postFixes` + * to the `path` and checking if the file exists on disk. + * @returns An absolute path to the first matching existing file, or `null` if none exist. + */ + private resolvePath(path: string, postFixes: string[]): AbsoluteFsPath|null { + for (const postFix of postFixes) { + const testPath = path + postFix; + if (fs.existsSync(testPath)) { + return AbsoluteFsPath.from(testPath); + } + } + return null; + } + + /** + * Can we consider the given path as an entry-point to a package? + * + * This is achieved by checking for the existence of `${modulePath}/package.json`. + */ + private isEntryPoint(modulePath: AbsoluteFsPath): boolean { + return fs.existsSync(AbsoluteFsPath.join(modulePath, 'package.json')); + } + + /** + * Apply the `pathMappers` to the `moduleName` and return all the possible + * paths that match. + * + * The mapped path is computed for each template in `mapping.templates` by + * replacing the `matcher.prefix` and `matcher.postfix` strings in `path with the + * `template.prefix` and `template.postfix` strings. + */ + private findMappedPaths(moduleName: string): AbsoluteFsPath[] { + const matches = this.pathMappings.map(mapping => this.matchMapping(moduleName, mapping)); + + let bestMapping: ProcessedPathMapping|undefined; + let bestMatch: string|undefined; + + for (let index = 0; index < this.pathMappings.length; index++) { + const mapping = this.pathMappings[index]; + const match = matches[index]; + if (match !== null) { + // If this mapping had no wildcard then this must be a complete match. + if (!mapping.matcher.hasWildcard) { + bestMatch = match; + bestMapping = mapping; + break; + } + // The best matched mapping is the one with the longest prefix. + if (!bestMapping || mapping.matcher.prefix > bestMapping.matcher.prefix) { + bestMatch = match; + bestMapping = mapping; + } + } + } + + return (bestMapping && bestMatch) ? this.computeMappedTemplates(bestMapping, bestMatch) : []; + } + + /** + * Attempt to find a mapped path for the given `path` and a `mapping`. + * + * The `path` matches the `mapping` if if it starts with `matcher.prefix` and ends with + * `matcher.postfix`. + * + * @returns the wildcard segment of a matched `path`, or `null` if no match. + */ + private matchMapping(path: string, mapping: ProcessedPathMapping): string|null { + const {prefix, postfix, hasWildcard} = mapping.matcher; + if (path.startsWith(prefix) && path.endsWith(postfix)) { + return hasWildcard ? path.substring(prefix.length, path.length - postfix.length) : ''; + } + return null; + } + + /** + * Compute the candidate paths from the given mapping's templates using the matched + * string. + */ + private computeMappedTemplates(mapping: ProcessedPathMapping, match: string) { + return mapping.templates.map( + template => + AbsoluteFsPath.resolve(mapping.baseUrl, template.prefix + match + template.postfix)); + } + + /** + * Search up the folder tree for the first folder that contains `package.json` + * or `null` if none is found. + */ + private findPackagePath(path: AbsoluteFsPath): AbsoluteFsPath|null { + let folder = path; + while (folder !== '/') { + folder = AbsoluteFsPath.dirname(folder); + if (fs.existsSync(AbsoluteFsPath.join(folder, 'package.json'))) { + return folder; + } + } + return null; + } +} + +/** The result of resolving an import to a module. */ +export type ResolvedModule = ResolvedExternalModule | ResolvedRelativeModule | ResolvedDeepImport; + +/** + * A module that is external to the package doing the importing. + * In this case we capture the folder containing the entry-point. + */ +export class ResolvedExternalModule { + constructor(public entryPointPath: AbsoluteFsPath) {} +} + +/** + * A module that is relative to the module doing the importing, and so internal to the + * source module's package. + */ +export class ResolvedRelativeModule { + constructor(public modulePath: AbsoluteFsPath) {} +} + +/** + * A module that is external to the package doing the importing but pointing to a + * module that is deep inside a package, rather than to an entry-point of the package. + */ +export class ResolvedDeepImport { + constructor(public importPath: AbsoluteFsPath) {} +} + +function splitOnStar(str: string): PathMappingPattern { + const [prefix, postfix] = str.split('*', 2); + return {prefix, postfix: postfix || '', hasWildcard: postfix !== undefined}; +} + +interface ProcessedPathMapping { + baseUrl: AbsoluteFsPath; + matcher: PathMappingPattern; + templates: PathMappingPattern[]; +} + +interface PathMappingPattern { + prefix: string; + postfix: string; + hasWildcard: boolean; +} diff --git a/packages/compiler-cli/ngcc/src/utils.ts b/packages/compiler-cli/ngcc/src/utils.ts index cce404ad3d5b..9cb91a2c097e 100644 --- a/packages/compiler-cli/ngcc/src/utils.ts +++ b/packages/compiler-cli/ngcc/src/utils.ts @@ -51,3 +51,17 @@ export function hasNameIdentifier(declaration: ts.Declaration): declaration is t const namedDeclaration: ts.Declaration&{name?: ts.Node} = declaration; return namedDeclaration.name !== undefined && ts.isIdentifier(namedDeclaration.name); } + +export type PathMappings = { + baseUrl: string, + paths: {[key: string]: string[]} +}; + +/** + * Test whether a path is "relative". + * + * Relative paths start with `/`, `./` or `../`; or are simply `.` or `..`. + */ +export function isRelativePath(path: string): boolean { + return /^\/|^\.\.?($|\/)/.test(path); +} diff --git a/packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts index 0a6889bff3af..cdf58f2359c1 100644 --- a/packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts @@ -5,24 +5,18 @@ * 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 {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DependencyHost} from '../../src/packages/dependency_host'; -const Module = require('module'); - -interface DepMap { - [path: string]: {resolved: string[], missing: string[]}; -} +import {ModuleResolver} from '../../src/packages/module_resolver'; const _ = AbsoluteFsPath.from; describe('DependencyHost', () => { let host: DependencyHost; - beforeEach(() => host = new DependencyHost()); + beforeEach(() => host = new DependencyHost(new ModuleResolver())); describe('getDependencies()', () => { beforeEach(createMockFileSystem); @@ -31,47 +25,36 @@ describe('DependencyHost', () => { 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(), new Set()); + host.computeDependencies(_('/no/imports/or/re-exports/index.js')); expect(ts.createSourceFile).not.toHaveBeenCalled(); }); it('should resolve all the external imports of the source file', () => { - spyOn(host, 'tryResolveEntryPoint') - .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); - const resolved = new Set(); - const missing = new Set(); - const deepImports = new Set(); - host.computeDependencies(_('/external/imports.js'), resolved, missing, deepImports); - expect(resolved.size).toBe(2); - expect(resolved.has('RESOLVED/path/to/x')).toBe(true); - expect(resolved.has('RESOLVED/path/to/y')).toBe(true); + const {dependencies, missing, deepImports} = + host.computeDependencies(_('/external/imports/index.js')); + expect(dependencies.size).toBe(2); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib-1/sub-1'))).toBe(true); }); it('should resolve all the external re-exports of the source file', () => { - spyOn(host, 'tryResolveEntryPoint') - .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); - const resolved = new Set(); - const missing = new Set(); - const deepImports = new Set(); - host.computeDependencies(_('/external/re-exports.js'), resolved, missing, deepImports); - expect(resolved.size).toBe(2); - expect(resolved.has('RESOLVED/path/to/x')).toBe(true); - expect(resolved.has('RESOLVED/path/to/y')).toBe(true); + const {dependencies, missing, deepImports} = + host.computeDependencies(_('/external/re-exports/index.js')); + expect(dependencies.size).toBe(2); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib-1/sub-1'))).toBe(true); }); it('should capture missing external imports', () => { - spyOn(host, 'tryResolveEntryPoint') - .and.callFake( - (from: string, importPath: string) => - importPath === 'missing' ? null : `RESOLVED/${importPath}`); - spyOn(host, 'tryResolve').and.callFake(() => null); - const resolved = new Set(); - const missing = new Set(); - const deepImports = new Set(); - host.computeDependencies(_('/external/imports-missing.js'), resolved, missing, deepImports); - expect(resolved.size).toBe(1); - expect(resolved.has('RESOLVED/path/to/x')).toBe(true); + const {dependencies, missing, deepImports} = + host.computeDependencies(_('/external/imports-missing/index.js')); + + expect(dependencies.size).toBe(1); + expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); expect(missing.size).toBe(1); expect(missing.has('missing')).toBe(true); expect(deepImports.size).toBe(0); @@ -81,125 +64,113 @@ describe('DependencyHost', () => { // This scenario verifies the behavior of the dependency analysis when an external import // is found that does not map to an entry-point but still exists on disk, i.e. a deep import. // Such deep imports are captured for diagnostics purposes. - const tryResolveEntryPoint = (from: string, importPath: string) => - importPath === 'deep/import' ? null : `RESOLVED/${importPath}`; - spyOn(host, 'tryResolveEntryPoint').and.callFake(tryResolveEntryPoint); - spyOn(host, 'tryResolve') - .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); - const resolved = new Set(); - const missing = new Set(); - const deepImports = new Set(); - host.computeDependencies(_('/external/deep-import.js'), resolved, missing, deepImports); - expect(resolved.size).toBe(0); + const {dependencies, missing, deepImports} = + host.computeDependencies(_('/external/deep-import/index.js')); + + expect(dependencies.size).toBe(0); expect(missing.size).toBe(0); expect(deepImports.size).toBe(1); - expect(deepImports.has('deep/import')).toBe(true); + expect(deepImports.has('/node_modules/lib-1/deep/import')).toBe(true); }); it('should recurse into internal dependencies', () => { - spyOn(host, 'resolveInternal') - .and.callFake( - (from: string, importPath: string) => path.join('/internal', importPath + '.js')); - spyOn(host, 'tryResolveEntryPoint') - .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); - const getDependenciesSpy = spyOn(host, 'computeDependencies').and.callThrough(); - const resolved = new Set(); - const missing = new Set(); - const deepImports = new Set(); - host.computeDependencies(_('/internal/outer.js'), resolved, missing, deepImports); - expect(getDependenciesSpy) - .toHaveBeenCalledWith('/internal/outer.js', resolved, missing, deepImports); - expect(getDependenciesSpy) - .toHaveBeenCalledWith( - '/internal/inner.js', resolved, missing, deepImports, jasmine.any(Set)); - expect(resolved.size).toBe(1); - expect(resolved.has('RESOLVED/path/to/y')).toBe(true); - }); + const {dependencies, missing, deepImports} = + host.computeDependencies(_('/internal/outer/index.js')); + expect(dependencies.size).toBe(1); + expect(dependencies.has(_('/node_modules/lib-1/sub-1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); it('should handle circular internal dependencies', () => { - spyOn(host, 'resolveInternal') - .and.callFake( - (from: string, importPath: string) => path.join('/internal', importPath + '.js')); - spyOn(host, 'tryResolveEntryPoint') - .and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`); - const resolved = new Set(); - const missing = new Set(); - const deepImports = new Set(); - host.computeDependencies(_('/internal/circular-a.js'), resolved, missing, deepImports); - expect(resolved.size).toBe(2); - expect(resolved.has('RESOLVED/path/to/x')).toBe(true); - expect(resolved.has('RESOLVED/path/to/y')).toBe(true); + const {dependencies, missing, deepImports} = + host.computeDependencies(_('/internal/circular-a/index.js')); + expect(dependencies.size).toBe(2); + expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib-1/sub-1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + + it('should support `paths` alias mappings when resolving modules', () => { + host = new DependencyHost(new ModuleResolver({ + baseUrl: '/dist', + paths: { + '@app/*': ['*'], + '@lib/*/test': ['lib/*/test'], + } + })); + const {dependencies, missing, deepImports} = + host.computeDependencies(_('/path-alias/index.js')); + expect(dependencies.size).toBe(4); + expect(dependencies.has(_('/dist/components'))).toBe(true); + expect(dependencies.has(_('/dist/shared'))).toBe(true); + expect(dependencies.has(_('/dist/lib/shared/test'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); }); 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';`, - '/external/deep-import.js': `import {Y} from 'deep/import';`, - '/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';`, + '/no/imports/or/re-exports/index.js': '// some text but no import-like statements', + '/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', + '/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports/index.js': `import {X} from 'lib-1';\nimport {Y} from 'lib-1/sub-1';`, + '/external/imports/package.json': '{"esm2015": "./index.js"}', + '/external/imports/index.metadata.json': 'MOCK METADATA', + '/external/re-exports/index.js': `export {X} from 'lib-1';\nexport {Y} from 'lib-1/sub-1';`, + '/external/re-exports/package.json': '{"esm2015": "./index.js"}', + '/external/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports-missing/index.js': + `import {X} from 'lib-1';\nimport {Y} from 'missing';`, + '/external/imports-missing/package.json': '{"esm2015": "./index.js"}', + '/external/imports-missing/index.metadata.json': 'MOCK METADATA', + '/external/deep-import/index.js': `import {Y} from 'lib-1/deep/import';`, + '/external/deep-import/package.json': '{"esm2015": "./index.js"}', + '/external/deep-import/index.metadata.json': 'MOCK METADATA', + '/internal/outer/index.js': `import {X} from '../inner';`, + '/internal/outer/package.json': '{"esm2015": "./index.js"}', + '/internal/outer/index.metadata.json': 'MOCK METADATA', + '/internal/inner/index.js': `import {Y} from 'lib-1/sub-1'; export declare class X {}`, + '/internal/circular-a/index.js': + `import {B} from '../circular-b'; import {X} from '../circular-b'; export {Y} from 'lib-1/sub-1';`, + '/internal/circular-b/index.js': + `import {A} from '../circular-a'; import {Y} from '../circular-a'; export {X} from 'lib-1';`, + '/internal/circular-a/package.json': '{"esm2015": "./index.js"}', + '/internal/circular-a/index.metadata.json': 'MOCK METADATA', + '/re-directed/index.js': `import {Z} from 'lib-1/sub-2';`, + '/re-directed/package.json': '{"esm2015": "./index.js"}', + '/re-directed/index.metadata.json': 'MOCK METADATA', + '/path-alias/index.js': + `import {TestHelper} from '@app/components';\nimport {Service} from '@app/shared';\nimport {TestHelper} from '@lib/shared/test';\nimport {X} from 'lib-1';`, + '/path-alias/package.json': '{"esm2015": "./index.js"}', + '/path-alias/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib-1/index.js': 'export declare class X {}', + '/node_modules/lib-1/package.json': '{"esm2015": "./index.js"}', + '/node_modules/lib-1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib-1/deep/import/index.js': 'export declare class DeepImport {}', + '/node_modules/lib-1/sub-1/index.js': 'export declare class Y {}', + '/node_modules/lib-1/sub-1/package.json': '{"esm2015": "./index.js"}', + '/node_modules/lib-1/sub-1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib-1/sub-2.js': `export * from './sub-2/sub-2';`, + '/node_modules/lib-1/sub-2/sub-2.js': `export declare class Z {}';`, + '/node_modules/lib-1/sub-2/package.json': '{"esm2015": "./sub-2.js"}', + '/node_modules/lib-1/sub-2/sub-2.metadata.json': 'MOCK METADATA', + '/dist/components/index.js': `class MyComponent {};`, + '/dist/components/package.json': '{"esm2015": "./index.js"}', + '/dist/components/index.metadata.json': 'MOCK METADATA', + '/dist/shared/index.js': `import {X} from 'lib-1';\nexport class Service {}`, + '/dist/shared/package.json': '{"esm2015": "./index.js"}', + '/dist/shared/index.metadata.json': 'MOCK METADATA', + '/dist/lib/shared/test/index.js': `export class TestHelper {}`, + '/dist/lib/shared/test/package.json': '{"esm2015": "./index.js"}', + '/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA', }); } - }); - - describe('resolveInternal', () => { - it('should resolve the dependency via `Module._resolveFilename`', () => { - spyOn(Module, '_resolveFilename').and.returnValue('/RESOLVED_PATH'); - const result = host.resolveInternal( - _('/SOURCE/PATH/FILE'), PathSegment.fromFsPath('../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'), PathSegment.fromFsPath('../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.tryResolveEntryPoint(_('/SOURCE_PATH'), PathSegment.fromFsPath('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.tryResolveEntryPoint(_('/SOURCE_PATH'), PathSegment.fromFsPath('TARGET_PATH'))) - .toEqual(_('/PATH/TO')); - }); - it('should return null if `tryResolve` returns null', () => { - spyOn(host, 'tryResolve').and.returnValue(null); - expect(host.tryResolveEntryPoint(_('/SOURCE_PATH'), PathSegment.fromFsPath('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'), PathSegment.fromFsPath('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'), PathSegment.fromFsPath('TARGET_PATH')); - expect(result).toBe(null); - }); + function restoreRealFileSystem() { mockFs.restore(); } }); describe('isStringImportOrReexport', () => { @@ -257,6 +228,4 @@ describe('DependencyHost', () => { .toBe(false); }); }); - - function restoreRealFileSystem() { mockFs.restore(); } -}); +}); \ No newline at end of file diff --git a/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts index 1618ab8f0e23..62639ed11c44 100644 --- a/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts @@ -9,6 +9,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DependencyHost} from '../../src/packages/dependency_host'; import {DependencyResolver, SortedEntryPointsInfo} from '../../src/packages/dependency_resolver'; import {EntryPoint} from '../../src/packages/entry_point'; +import {ModuleResolver} from '../../src/packages/module_resolver'; import {MockLogger} from '../helpers/mock_logger'; const _ = AbsoluteFsPath.from; @@ -17,7 +18,7 @@ describe('DependencyResolver', () => { let host: DependencyHost; let resolver: DependencyResolver; beforeEach(() => { - host = new DependencyHost(); + host = new DependencyHost(new ModuleResolver()); resolver = new DependencyResolver(new MockLogger(), host); }); describe('sortEntryPointsByDependency()', () => { diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts index 92967be7702a..15b615e87b95 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts @@ -13,6 +13,7 @@ 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'; +import {ModuleResolver} from '../../src/packages/module_resolver'; import {MockLogger} from '../helpers/mock_logger'; const _ = AbsoluteFsPath.from; @@ -21,7 +22,7 @@ describe('findEntryPoints()', () => { let resolver: DependencyResolver; let finder: EntryPointFinder; beforeEach(() => { - resolver = new DependencyResolver(new MockLogger(), new DependencyHost()); + resolver = new DependencyResolver(new MockLogger(), new DependencyHost(new ModuleResolver())); spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []}; }); diff --git a/packages/compiler-cli/ngcc/test/packages/module_resolver_spec.ts b/packages/compiler-cli/ngcc/test/packages/module_resolver_spec.ts new file mode 100644 index 000000000000..adc8ac33e7f2 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/packages/module_resolver_spec.ts @@ -0,0 +1,212 @@ +/** + * @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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {ModuleResolver, ResolvedDeepImport, ResolvedExternalModule, ResolvedRelativeModule} from '../../src/packages/module_resolver'; + +const _ = AbsoluteFsPath.from; + +function createMockFileSystem() { + mockFs({ + '/libs': { + 'local-package': { + 'package.json': 'PACKAGE.JSON for local-package', + 'index.js': `import {X} from './x';`, + 'x.js': `export class X {}`, + 'sub-folder': { + 'index.js': `import {X} from '../x';`, + }, + 'node_modules': { + 'package-1': { + 'sub-folder': {'index.js': `export class Z {}`}, + 'package.json': 'PACKAGE.JSON for package-1', + }, + }, + }, + 'node_modules': { + 'package-2': { + 'package.json': 'PACKAGE.JSON for package-2', + 'node_modules': { + 'package-3': { + 'package.json': 'PACKAGE.JSON for package-3', + }, + }, + }, + }, + }, + '/dist': { + 'package-4': { + 'x.js': `export class X {}`, + 'package.json': 'PACKAGE.JSON for package-4', + 'sub-folder': {'index.js': `import {X} from '@shared/package-4/x';`}, + }, + 'sub-folder': { + 'package-4': { + 'package.json': 'PACKAGE.JSON for package-4', + }, + 'package-5': { + 'package.json': 'PACKAGE.JSON for package-5', + 'post-fix': { + 'package.json': 'PACKAGE.JSON for package-5/post-fix', + } + }, + } + }, + '/node_modules': { + 'top-package': { + 'package.json': 'PACKAGE.JSON for top-package', + } + } + }); +} + +function restoreRealFileSystem() { + mockFs.restore(); +} + +describe('ModuleResolver', () => { + beforeEach(createMockFileSystem); + afterEach(restoreRealFileSystem); + + describe('resolveModule()', () => { + describe('with relative paths', () => { + it('should resolve sibling, child and aunt modules', () => { + const resolver = new ModuleResolver(); + expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); + expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedRelativeModule(_('/libs/local-package/sub-folder/index.js'))); + expect(resolver.resolveModuleImport('../x', _('/libs/local-package/sub-folder/index.js'))) + .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); + }); + + it('should return `null` if the resolved module relative module does not exist', () => { + const resolver = new ModuleResolver(); + expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null); + }); + }); + + describe('with non-mapped external paths', () => { + it('should resolve to the package.json of a local node_modules package', () => { + const resolver = new ModuleResolver(); + expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); + expect( + resolver.resolveModuleImport('package-1', _('/libs/local-package/sub-folder/index.js'))) + .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); + expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/x.js'))) + .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); + }); + + it('should resolve to the package.json of a higher node_modules package', () => { + const resolver = new ModuleResolver(); + expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/libs/node_modules/package-2'))); + expect(resolver.resolveModuleImport('top-package', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/node_modules/top-package'))); + }); + + it('should return `null` if the package cannot be found', () => { + const resolver = new ModuleResolver(); + expect(resolver.resolveModuleImport('missing-2', _('/libs/local-package/index.js'))) + .toBe(null); + }); + + it('should return `null` if the package is not accessible because it is in a inner node_modules package', + () => { + const resolver = new ModuleResolver(); + expect(resolver.resolveModuleImport('package-3', _('/libs/local-package/index.js'))) + .toBe(null); + }); + + it('should identify deep imports into an external module', () => { + const resolver = new ModuleResolver(); + expect( + resolver.resolveModuleImport('package-1/sub-folder', _('/libs/local-package/index.js'))) + .toEqual( + new ResolvedDeepImport(_('/libs/local-package/node_modules/package-1/sub-folder'))); + }); + }); + + describe('with mapped path external modules', () => { + it('should resolve to the package.json of simple mapped packages', () => { + const resolver = + new ModuleResolver({baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); + + expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); + + expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5'))); + }); + + it('should select the best match by the length of prefix before the *', () => { + const resolver = new ModuleResolver({ + baseUrl: '/dist', + paths: { + '@lib/*': ['*'], + '@lib/sub-folder/*': ['*'], + } + }); + + // We should match the second path (e.g. `'@lib/sub-folder/*'`), which will actually map to + // `*` and so the final resolved path will not include the `sub-folder` segment. + expect(resolver.resolveModuleImport( + '@lib/sub-folder/package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); + }); + + it('should follow the ordering of `paths` when matching mapped packages', () => { + let resolver: ModuleResolver; + + resolver = new ModuleResolver({baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); + expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); + + resolver = new ModuleResolver({baseUrl: '/dist', paths: {'*': ['sub-folder/*', '*']}}); + expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); + }); + + it('should resolve packages when the path mappings have post-fixes', () => { + const resolver = + new ModuleResolver({baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}}); + expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); + }); + + it('should match paths against complex path matchers', () => { + const resolver = + new ModuleResolver({baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}}); + expect(resolver.resolveModuleImport('@shared/package-4', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); + expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) + .toBe(null); + }); + + it('should resolve path as "relative" if the mapped path is inside the current package', + () => { + const resolver = new ModuleResolver({baseUrl: '/dist', paths: {'@shared/*': ['*']}}); + expect(resolver.resolveModuleImport( + '@shared/package-4/x', _('/dist/package-4/sub-folder/index.js'))) + .toEqual(new ResolvedRelativeModule(_('/dist/package-4/x.js'))); + }); + + it('should resolve paths where the wildcard matches more than one path segment', () => { + const resolver = + new ModuleResolver({baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}}); + expect( + resolver.resolveModuleImport( + '@shared/sub-folder/package-5/post-fix', _('/dist/package-4/sub-folder/index.js'))) + .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); + }); + }); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/utils_spec.ts b/packages/compiler-cli/ngcc/test/utils_spec.ts new file mode 100644 index 000000000000..4b44d7e6d2e3 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/utils_spec.ts @@ -0,0 +1,36 @@ +/** + * @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 {isRelativePath} from '../src/utils'; + +describe('isRelativePath()', () => { + it('should return true for relative paths', () => { + expect(isRelativePath('.')).toBe(true); + expect(isRelativePath('..')).toBe(true); + expect(isRelativePath('./')).toBe(true); + expect(isRelativePath('../')).toBe(true); + expect(isRelativePath('./abc/xyz')).toBe(true); + expect(isRelativePath('../abc/xyz')).toBe(true); + }); + + it('should return true for absolute paths', () => { + expect(isRelativePath('/')).toBe(true); + expect(isRelativePath('/abc/xyz')).toBe(true); + }); + + it('should return false for other paths', () => { + expect(isRelativePath('abc')).toBe(false); + expect(isRelativePath('abc/xyz')).toBe(false); + expect(isRelativePath('.abc')).toBe(false); + expect(isRelativePath('..abc')).toBe(false); + expect(isRelativePath('@abc')).toBe(false); + expect(isRelativePath('.abc/xyz')).toBe(false); + expect(isRelativePath('..abc/xyz')).toBe(false); + expect(isRelativePath('@abc/xyz')).toBe(false); + }); +}); From 1f7a7eca796b853b9fa49916b5f5f848abe05c1b Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:57 +0100 Subject: [PATCH 07/13] feat(ivy): add helper methods to AbsoluteFsPath --- .../compiler-cli/src/ngtsc/path/src/types.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/compiler-cli/src/ngtsc/path/src/types.ts b/packages/compiler-cli/src/ngtsc/path/src/types.ts index 0e2188bf0e88..6561d7e2feee 100644 --- a/packages/compiler-cli/src/ngtsc/path/src/types.ts +++ b/packages/compiler-cli/src/ngtsc/path/src/types.ts @@ -5,7 +5,7 @@ * 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 'path'; import * as ts from 'typescript'; import {isAbsolutePath, normalizeSeparators} from './util'; @@ -62,6 +62,24 @@ export const AbsoluteFsPath = { // ts.SourceFile paths are always absolute. return sf.fileName as AbsoluteFsPath; }, + + /** + * Wrapper around `path.dirname` that returns an absolute path. + */ + dirname: function(file: AbsoluteFsPath): + AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked(path.dirname(file));}, + + /** + * Wrapper around `path.join` that returns an absolute path. + */ + join: function(basePath: AbsoluteFsPath, ...paths: string[]): + AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked(path.posix.join(basePath, ...paths));}, + + /** + * Wrapper around `path.resolve` that returns an absolute paths. + */ + resolve: function(basePath: string, ...paths: string[]): + AbsoluteFsPath { return AbsoluteFsPath.from(path.resolve(basePath, ...paths));}, }; /** @@ -83,4 +101,13 @@ export const PathSegment = { * Convert the path `str` to a `PathSegment`, while assuming that `str` is already normalized. */ fromUnchecked: function(str: string): PathSegment { return str as PathSegment;}, + + /** + * Wrapper around `path.relative` that returns a `PathSegment`. + */ + relative: function(from: AbsoluteFsPath, to: AbsoluteFsPath): + PathSegment { return PathSegment.fromFsPath(path.relative(from, to));}, + + basename: function(filePath: string, extension?: string): + PathSegment { return path.basename(filePath, extension) as PathSegment;} }; From 6d2301396c06c15b457b3f2ce975e8dbab50fb11 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:57 +0100 Subject: [PATCH 08/13] feat(ivy): ngcc - support additional paths to process By passing a `pathMappings` configuration (a subset of the `ts.CompilerOptions` interface), we can instuct ngcc to process additional paths outside the `node_modules` folder. --- packages/compiler-cli/ngcc/index.ts | 1 + packages/compiler-cli/ngcc/src/main.ts | 24 +++--- .../ngcc/src/packages/entry_point_bundle.ts | 10 +-- .../ngcc/src/packages/entry_point_finder.ts | 76 ++++++++++++++++++- .../ngcc/test/integration/ngcc_spec.ts | 39 +++++++++- 5 files changed, 129 insertions(+), 21 deletions(-) diff --git a/packages/compiler-cli/ngcc/index.ts b/packages/compiler-cli/ngcc/index.ts index dc63ad76b857..8b131672cae4 100644 --- a/packages/compiler-cli/ngcc/index.ts +++ b/packages/compiler-cli/ngcc/index.ts @@ -12,6 +12,7 @@ import {EntryPointJsonProperty, EntryPointPackageJson} from './src/packages/entr export {ConsoleLogger, LogLevel} from './src/logging/console_logger'; export {Logger} from './src/logging/logger'; export {NgccOptions, mainNgcc as process} from './src/main'; +export {PathMappings} from './src/utils'; export function hasBeenProcessed(packageJson: object, format: string) { // We are wrapping this function to hide the internal types. diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index acee60c7c609..c99a7bf56d00 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -21,12 +21,12 @@ import {makeEntryPointBundle} from './packages/entry_point_bundle'; import {EntryPointFinder} from './packages/entry_point_finder'; import {ModuleResolver} from './packages/module_resolver'; import {Transformer} from './packages/transformer'; +import {PathMappings} from './utils'; import {FileWriter} from './writing/file_writer'; import {InPlaceFileWriter} from './writing/in_place_file_writer'; import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer'; - /** * The options to configure the ngcc compiler. */ @@ -58,6 +58,11 @@ export interface NgccOptions { * Provide a logger that will be called with log messages. */ logger?: Logger; + /** + * Paths mapping configuration (`paths` and `baseUrl`), as found in `ts.CompilerOptions`. + * These are used to resolve paths to locally built Angular libraries. + */ + pathMappings?: PathMappings; } const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015']; @@ -70,12 +75,12 @@ const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015']; * * @param options The options telling ngcc what to compile and how. */ -export function mainNgcc({basePath, targetEntryPointPath, - propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, - compileAllFormats = true, createNewEntryPointFormats = false, - logger = new ConsoleLogger(LogLevel.info)}: NgccOptions): void { +export function mainNgcc( + {basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, + compileAllFormats = true, createNewEntryPointFormats = false, + logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void { const transformer = new Transformer(logger); - const moduleResolver = new ModuleResolver(); + const moduleResolver = new ModuleResolver(pathMappings); const host = new DependencyHost(moduleResolver); const resolver = new DependencyResolver(logger, host); const finder = new EntryPointFinder(logger, resolver); @@ -92,8 +97,8 @@ export function mainNgcc({basePath, targetEntryPointPath, return; } - const {entryPoints} = - finder.findEntryPoints(AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath); + const {entryPoints} = finder.findEntryPoints( + AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath, pathMappings); if (absoluteTargetEntryPointPath && entryPoints.length === 0) { markNonAngularPackageAsProcessed(absoluteTargetEntryPointPath, propertiesToConsider); @@ -132,7 +137,8 @@ export function mainNgcc({basePath, targetEntryPointPath, // the property as processed even if its underlying format has been built already. if (!compiledFormats.has(formatPath) && (compileAllFormats || isFirstFormat)) { const bundle = makeEntryPointBundle( - entryPoint.path, formatPath, entryPoint.typings, isCore, property, format, processDts); + entryPoint.path, formatPath, entryPoint.typings, isCore, property, format, processDts, + pathMappings); if (bundle) { logger.info(`Compiling ${entryPoint.name} : ${property} as ${format}`); const transformedFiles = transformer.transform(bundle); diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts index 0130d5267748..778577e30529 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts @@ -9,11 +9,11 @@ import {resolve} from 'canonical-path'; import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {PathMappings} from '../utils'; + import {BundleProgram, makeBundleProgram} from './bundle_program'; import {EntryPointFormat, EntryPointJsonProperty} from './entry_point'; - - /** * A bundle of files and paths (and TS programs) that correspond to a particular * format of a package entry-point. @@ -39,13 +39,13 @@ export interface EntryPointBundle { */ export function makeEntryPointBundle( entryPointPath: string, formatPath: string, typingsPath: string, isCore: boolean, - formatProperty: EntryPointJsonProperty, format: EntryPointFormat, - transformDts: boolean): EntryPointBundle|null { + formatProperty: EntryPointJsonProperty, format: EntryPointFormat, transformDts: boolean, + pathMappings?: PathMappings): EntryPointBundle|null { // Create the TS program and necessary helpers. const options: ts.CompilerOptions = { allowJs: true, maxNodeModuleJsDepth: Infinity, - rootDir: entryPointPath, + rootDir: entryPointPath, ...pathMappings }; const host = ts.createCompilerHost(options); const rootDirs = [AbsoluteFsPath.from(entryPointPath)]; diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts index bbb2f51778e1..2daa0021592d 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts @@ -7,9 +7,11 @@ */ import * as path from 'canonical-path'; import * as fs from 'fs'; +import {join, resolve} from 'path'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {Logger} from '../logging/logger'; +import {PathMappings} from '../utils'; import {DependencyResolver, SortedEntryPointsInfo} from './dependency_resolver'; import {EntryPoint, getEntryPointInfo} from './entry_point'; @@ -21,15 +23,51 @@ export class EntryPointFinder { * 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: AbsoluteFsPath, targetEntryPointPath?: AbsoluteFsPath): - SortedEntryPointsInfo { - const unsortedEntryPoints = this.walkDirectoryForEntryPoints(sourceDirectory); + findEntryPoints( + sourceDirectory: AbsoluteFsPath, targetEntryPointPath?: AbsoluteFsPath, + pathMappings?: PathMappings): SortedEntryPointsInfo { + const basePaths = this.getBasePaths(sourceDirectory, pathMappings); + const unsortedEntryPoints = basePaths.reduce( + (entryPoints, basePath) => entryPoints.concat(this.walkDirectoryForEntryPoints(basePath)), + []); const targetEntryPoint = targetEntryPointPath ? unsortedEntryPoints.find(entryPoint => entryPoint.path === targetEntryPointPath) : undefined; return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints, targetEntryPoint); } + /** + * Extract all the base-paths that we need to search for entry-points. + * + * This always contains the standard base-path (`sourceDirectory`). + * But it also parses the `paths` mappings object to guess additional base-paths. + * + * For example: + * + * ``` + * getBasePaths('/node_modules', {baseUrl: '/dist', paths: {'*': ['lib/*', 'lib/generated/*']}}) + * > ['/node_modules', '/dist/lib'] + * ``` + * + * Notice that `'/dist'` is not included as there is no `'*'` path, + * and `'/dist/lib/generated'` is not included as it is covered by `'/dist/lib'`. + * + * @param sourceDirectory The standard base-path (e.g. node_modules). + * @param pathMappings Path mapping configuration, from which to extract additional base-paths. + */ + private getBasePaths(sourceDirectory: AbsoluteFsPath, pathMappings?: PathMappings): + AbsoluteFsPath[] { + const basePaths = [sourceDirectory]; + if (pathMappings) { + const baseUrl = AbsoluteFsPath.from(resolve(pathMappings.baseUrl)); + values(pathMappings.paths).forEach(paths => paths.forEach(path => { + basePaths.push(AbsoluteFsPath.fromUnchecked(join(baseUrl, extractPathPrefix(path)))); + })); + } + basePaths.sort(); // Get the paths in order with the shorter ones first. + return basePaths.filter(removeDeeperPaths); + } + /** * 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/...`. @@ -117,3 +155,35 @@ export class EntryPointFinder { }); } } + +/** + * Extract everything in the `path` up to the first `*`. + * @param path The path to parse. + * @returns The extracted prefix. + */ +function extractPathPrefix(path: string) { + return path.split('*', 1)[0]; +} + +/** + * A filter function that removes paths that are already covered by higher paths. + * + * @param value The current path. + * @param index The index of the current path. + * @param array The array of paths (sorted alphabetically). + * @returns true if this path is not already covered by a previous path. + */ +function removeDeeperPaths(value: AbsoluteFsPath, index: number, array: AbsoluteFsPath[]) { + for (let i = 0; i < index; i++) { + if (value.startsWith(array[i])) return false; + } + return true; +} + +/** + * Extract all the values (not keys) from an object. + * @param obj The object to process. + */ +function values(obj: {[key: string]: T}): T[] { + return Object.keys(obj).map(key => obj[key]); +} diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 6500dc9fc979..1ad2c9f6aa8f 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -309,6 +309,24 @@ describe('ngcc main()', () => { expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']); }); }); + + describe('with pathMappings', () => { + it('should find and compile packages accessible via the pathMappings', () => { + mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['es2015'], + pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'}, + }); + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ + es2015: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + expect(loadPackage('local-package', '/dist').__processed_by_ivy_ngcc__).toEqual({ + es2015: '0.0.0-PLACEHOLDER', + typings: '0.0.0-PLACEHOLDER', + }); + }); + }); }); @@ -321,10 +339,23 @@ function createMockFileSystem() { 'package.json': '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}', // no metadata.json file so not compiled by Angular. 'index.js': - 'import {AppModule} from "@angular/common"; export class MyApp extends AppModule;', + 'import {AppModule} from "@angular/common"; export class MyApp extends AppModule {};', 'index.d.ts': 'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;', - } + }, + '/dist/local-package': { + 'package.json': + '{"name": "local-package", "es2015": "./index.js", "typings": "./index.d.ts"}', + 'index.metadata.json': 'DUMMY DATA', + 'index.js': ` + import {Component} from '@angular/core'; + export class AppComponent {}; + AppComponent.decorators = [ + { type: Component, args: [{selector: 'app', template: '

Hello

'}] } + ];`, + 'index.d.ts': ` + export declare class AppComponent {};`, + }, }); } @@ -367,6 +398,6 @@ interface Directory { [pathSegment: string]: string|Directory; } -function loadPackage(packageName: string): EntryPointPackageJson { - return JSON.parse(readFileSync(`/node_modules/${packageName}/package.json`, 'utf8')); +function loadPackage(packageName: string, basePath = '/node_modules'): EntryPointPackageJson { + return JSON.parse(readFileSync(`${basePath}/${packageName}/package.json`, 'utf8')); } From a4abe70342f3c92b1c712ba5cd14f57290c3a172 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:57 +0100 Subject: [PATCH 09/13] refactor(ivy): ngcc - move the dependency resolving stuff around For UMD/RequireJS support we will need to have multiple `DependencyHost` implementations. This commit prepares the ground for that. --- .../ngcc/src/dependencies/dependency_host.ts | 19 ++++++++ .../dependency_resolver.ts | 5 +- .../esm_dependency_host.ts} | 46 +++++++++++++------ .../module_resolver.ts | 0 packages/compiler-cli/ngcc/src/main.ts | 9 ++-- .../ngcc/src/packages/entry_point_finder.ts | 2 +- .../dependency_resolver_spec.ts | 22 ++++----- .../esm_dependency_host_spec.ts} | 27 ++++++----- .../module_resolver_spec.ts | 2 +- .../test/packages/entry_point_finder_spec.ts | 9 ++-- 10 files changed, 89 insertions(+), 52 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts rename packages/compiler-cli/ngcc/src/{packages => dependencies}/dependency_resolver.ts (98%) rename packages/compiler-cli/ngcc/src/{packages/dependency_host.ts => dependencies/esm_dependency_host.ts} (71%) rename packages/compiler-cli/ngcc/src/{packages => dependencies}/module_resolver.ts (100%) rename packages/compiler-cli/ngcc/test/{packages => dependencies}/dependency_resolver_spec.ts (88%) rename packages/compiler-cli/ngcc/test/{packages/dependency_host_spec.ts => dependencies/esm_dependency_host_spec.ts} (92%) rename packages/compiler-cli/ngcc/test/{packages => dependencies}/module_resolver_spec.ts (99%) diff --git a/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts new file mode 100644 index 000000000000..b2210eb39c7f --- /dev/null +++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts @@ -0,0 +1,19 @@ +/** + * @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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; + +export interface DependencyHost { + findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo; +} + +export interface DependencyInfo { + dependencies: Set; + missing: Set; + deepImports: Set; +} diff --git a/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts similarity index 98% rename from packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts rename to packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts index 15e1c3d3f841..e289b5aabb79 100644 --- a/packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts @@ -11,9 +11,10 @@ import {resolve} from 'path'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {Logger} from '../logging/logger'; +import {EntryPoint, EntryPointJsonProperty, getEntryPointFormat} from '../packages/entry_point'; import {DependencyHost} from './dependency_host'; -import {EntryPoint, EntryPointJsonProperty, getEntryPointFormat} from './entry_point'; + /** @@ -119,7 +120,7 @@ export class DependencyResolver { // Now add the dependencies between them angularEntryPoints.forEach(entryPoint => { const entryPointPath = getEntryPointPath(entryPoint); - const {dependencies, missing, deepImports} = this.host.computeDependencies(entryPointPath); + const {dependencies, missing, deepImports} = this.host.findDependencies(entryPointPath); if (missing.size > 0) { // This entry point has dependencies that are missing diff --git a/packages/compiler-cli/ngcc/src/packages/dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts similarity index 71% rename from packages/compiler-cli/ngcc/src/packages/dependency_host.ts rename to packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts index 501d1ccc38cc..b71663f16741 100644 --- a/packages/compiler-cli/ngcc/src/packages/dependency_host.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts @@ -11,18 +11,37 @@ import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {DependencyHost, DependencyInfo} from './dependency_host'; import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; - /** * Helper functions for computing dependencies. */ -export class DependencyHost { +export class EsmDependencyHost implements DependencyHost { constructor(private moduleResolver: ModuleResolver) {} + + /** + * Find all the dependencies for the entry-point at the given path. + * + * @param entryPointPath The absolute path to the JavaScript file that represents an entry-point. + * @returns Information about the dependencies of the entry-point, including those that were + * missing or deep imports into other entry-points. + */ + findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo { + const dependencies = new Set(); + const missing = new Set(); + const deepImports = new Set(); + const alreadySeen = new Set(); + this.recursivelyFindDependencies( + entryPointPath, dependencies, missing, deepImports, alreadySeen); + return {dependencies, missing, deepImports}; + } + /** - * 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. + * Compute the dependencies of the given file. + * + * @param file An absolute path to the file whose dependencies we want to get. * @param dependencies A set that will have the absolute paths of resolved entry points added to * it. * @param missing A set that will have the dependencies that could not be found added to it. @@ -32,19 +51,17 @@ export class DependencyHost { * in a * circular dependency loop. */ - computeDependencies( - from: AbsoluteFsPath, dependencies: Set = new Set(), - missing: Set = new Set(), deepImports: Set = new Set(), - alreadySeen: Set = new Set()): - {dependencies: Set, missing: Set, deepImports: Set} { - const fromContents = fs.readFileSync(from, 'utf8'); + private recursivelyFindDependencies( + file: AbsoluteFsPath, dependencies: Set, missing: Set, + deepImports: Set, alreadySeen: Set): void { + const fromContents = fs.readFileSync(file, 'utf8'); if (!this.hasImportOrReexportStatements(fromContents)) { - return {dependencies, missing, deepImports}; + 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); + ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS); sf.statements // filter out statements that are not imports or reexports .filter(this.isStringImportOrReexport) @@ -52,13 +69,13 @@ export class DependencyHost { .map(stmt => stmt.moduleSpecifier.text) // Resolve this module id into an absolute path .forEach(importPath => { - const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, from); + const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, file); if (resolvedModule) { if (resolvedModule instanceof ResolvedRelativeModule) { const internalDependency = resolvedModule.modulePath; if (!alreadySeen.has(internalDependency)) { alreadySeen.add(internalDependency); - this.computeDependencies( + this.recursivelyFindDependencies( internalDependency, dependencies, missing, deepImports, alreadySeen); } } else { @@ -72,7 +89,6 @@ export class DependencyHost { missing.add(importPath); } }); - return {dependencies, missing, deepImports}; } /** diff --git a/packages/compiler-cli/ngcc/src/packages/module_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts similarity index 100% rename from packages/compiler-cli/ngcc/src/packages/module_resolver.ts rename to packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index c99a7bf56d00..8ed2a2faf2d6 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -11,15 +11,15 @@ import {readFileSync} from 'fs'; import {AbsoluteFsPath} from '../../src/ngtsc/path'; +import {DependencyResolver} from './dependencies/dependency_resolver'; +import {EsmDependencyHost} from './dependencies/esm_dependency_host'; +import {ModuleResolver} from './dependencies/module_resolver'; import {ConsoleLogger, LogLevel} from './logging/console_logger'; import {Logger} from './logging/logger'; import {hasBeenProcessed, markAsProcessed} from './packages/build_marker'; -import {DependencyHost} from './packages/dependency_host'; -import {DependencyResolver} from './packages/dependency_resolver'; import {EntryPointFormat, EntryPointJsonProperty, SUPPORTED_FORMAT_PROPERTIES, getEntryPointFormat} from './packages/entry_point'; import {makeEntryPointBundle} from './packages/entry_point_bundle'; import {EntryPointFinder} from './packages/entry_point_finder'; -import {ModuleResolver} from './packages/module_resolver'; import {Transformer} from './packages/transformer'; import {PathMappings} from './utils'; import {FileWriter} from './writing/file_writer'; @@ -27,6 +27,7 @@ import {InPlaceFileWriter} from './writing/in_place_file_writer'; import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer'; + /** * The options to configure the ngcc compiler. */ @@ -81,7 +82,7 @@ export function mainNgcc( logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void { const transformer = new Transformer(logger); const moduleResolver = new ModuleResolver(pathMappings); - const host = new DependencyHost(moduleResolver); + const host = new EsmDependencyHost(moduleResolver); const resolver = new DependencyResolver(logger, host); const finder = new EntryPointFinder(logger, resolver); const fileWriter = getFileWriter(createNewEntryPointFormats); diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts index 2daa0021592d..c52c0cbba113 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts @@ -10,10 +10,10 @@ import * as fs from 'fs'; import {join, resolve} from 'path'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver'; import {Logger} from '../logging/logger'; import {PathMappings} from '../utils'; -import {DependencyResolver, SortedEntryPointsInfo} from './dependency_resolver'; import {EntryPoint, getEntryPointInfo} from './entry_point'; diff --git a/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts similarity index 88% rename from packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts rename to packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts index 62639ed11c44..452aef03f5db 100644 --- a/packages/compiler-cli/ngcc/test/packages/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts @@ -6,19 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {DependencyHost} from '../../src/packages/dependency_host'; -import {DependencyResolver, SortedEntryPointsInfo} from '../../src/packages/dependency_resolver'; +import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver'; +import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; +import {ModuleResolver} from '../../src/dependencies/module_resolver'; import {EntryPoint} from '../../src/packages/entry_point'; -import {ModuleResolver} from '../../src/packages/module_resolver'; import {MockLogger} from '../helpers/mock_logger'; const _ = AbsoluteFsPath.from; describe('DependencyResolver', () => { - let host: DependencyHost; + let host: EsmDependencyHost; let resolver: DependencyResolver; beforeEach(() => { - host = new DependencyHost(new ModuleResolver()); + host = new EsmDependencyHost(new ModuleResolver()); resolver = new DependencyResolver(new MockLogger(), host); }); describe('sortEntryPointsByDependency()', () => { @@ -57,13 +57,13 @@ describe('DependencyResolver', () => { }; it('should order the entry points by their dependency on each other', () => { - spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies(dependencies)); + spyOn(host, 'findDependencies').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({ + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ [_('/first/index.js')]: {resolved: [], missing: ['/missing']}, [_('/second/sub/index.js')]: {resolved: [], missing: []}, })); @@ -75,7 +75,7 @@ describe('DependencyResolver', () => { }); it('should remove entry points that depended upon an invalid entry-point', () => { - spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({ + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ [_('/first/index.js')]: {resolved: [second.path], missing: []}, [_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']}, [_('/third/index.js')]: {resolved: [], missing: []}, @@ -90,7 +90,7 @@ describe('DependencyResolver', () => { }); it('should remove entry points that will depend upon an invalid entry-point', () => { - spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({ + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies({ [_('/first/index.js')]: {resolved: [second.path], missing: []}, [_('/second/sub/index.js')]: {resolved: [], missing: ['/missing']}, [_('/third/index.js')]: {resolved: [], missing: []}, @@ -111,7 +111,7 @@ describe('DependencyResolver', () => { }); it('should capture any dependencies that were ignored', () => { - spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies(dependencies)); + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); expect(result.ignoredDependencies).toEqual([ {entryPoint: first, dependencyPath: '/ignored-1'}, @@ -120,7 +120,7 @@ describe('DependencyResolver', () => { }); it('should only return dependencies of the target, if provided', () => { - spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies(dependencies)); + spyOn(host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); const entryPoints = [fifth, first, fourth, second, third]; let sorted: SortedEntryPointsInfo; diff --git a/packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts similarity index 92% rename from packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts rename to packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts index cdf58f2359c1..7c1612c739eb 100644 --- a/packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts @@ -9,14 +9,14 @@ import * as mockFs from 'mock-fs'; import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {DependencyHost} from '../../src/packages/dependency_host'; -import {ModuleResolver} from '../../src/packages/module_resolver'; +import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; +import {ModuleResolver} from '../../src/dependencies/module_resolver'; const _ = AbsoluteFsPath.from; describe('DependencyHost', () => { - let host: DependencyHost; - beforeEach(() => host = new DependencyHost(new ModuleResolver())); + let host: EsmDependencyHost; + beforeEach(() => host = new EsmDependencyHost(new ModuleResolver())); describe('getDependencies()', () => { beforeEach(createMockFileSystem); @@ -25,13 +25,13 @@ describe('DependencyHost', () => { 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/index.js')); + host.findDependencies(_('/no/imports/or/re-exports/index.js')); expect(ts.createSourceFile).not.toHaveBeenCalled(); }); it('should resolve all the external imports of the source file', () => { const {dependencies, missing, deepImports} = - host.computeDependencies(_('/external/imports/index.js')); + host.findDependencies(_('/external/imports/index.js')); expect(dependencies.size).toBe(2); expect(missing.size).toBe(0); expect(deepImports.size).toBe(0); @@ -41,7 +41,7 @@ describe('DependencyHost', () => { it('should resolve all the external re-exports of the source file', () => { const {dependencies, missing, deepImports} = - host.computeDependencies(_('/external/re-exports/index.js')); + host.findDependencies(_('/external/re-exports/index.js')); expect(dependencies.size).toBe(2); expect(missing.size).toBe(0); expect(deepImports.size).toBe(0); @@ -51,7 +51,7 @@ describe('DependencyHost', () => { it('should capture missing external imports', () => { const {dependencies, missing, deepImports} = - host.computeDependencies(_('/external/imports-missing/index.js')); + host.findDependencies(_('/external/imports-missing/index.js')); expect(dependencies.size).toBe(1); expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); @@ -65,7 +65,7 @@ describe('DependencyHost', () => { // is found that does not map to an entry-point but still exists on disk, i.e. a deep import. // Such deep imports are captured for diagnostics purposes. const {dependencies, missing, deepImports} = - host.computeDependencies(_('/external/deep-import/index.js')); + host.findDependencies(_('/external/deep-import/index.js')); expect(dependencies.size).toBe(0); expect(missing.size).toBe(0); @@ -75,7 +75,7 @@ describe('DependencyHost', () => { it('should recurse into internal dependencies', () => { const {dependencies, missing, deepImports} = - host.computeDependencies(_('/internal/outer/index.js')); + host.findDependencies(_('/internal/outer/index.js')); expect(dependencies.size).toBe(1); expect(dependencies.has(_('/node_modules/lib-1/sub-1'))).toBe(true); @@ -85,7 +85,7 @@ describe('DependencyHost', () => { it('should handle circular internal dependencies', () => { const {dependencies, missing, deepImports} = - host.computeDependencies(_('/internal/circular-a/index.js')); + host.findDependencies(_('/internal/circular-a/index.js')); expect(dependencies.size).toBe(2); expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); expect(dependencies.has(_('/node_modules/lib-1/sub-1'))).toBe(true); @@ -94,15 +94,14 @@ describe('DependencyHost', () => { }); it('should support `paths` alias mappings when resolving modules', () => { - host = new DependencyHost(new ModuleResolver({ + host = new EsmDependencyHost(new ModuleResolver({ baseUrl: '/dist', paths: { '@app/*': ['*'], '@lib/*/test': ['lib/*/test'], } })); - const {dependencies, missing, deepImports} = - host.computeDependencies(_('/path-alias/index.js')); + const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js')); expect(dependencies.size).toBe(4); expect(dependencies.has(_('/dist/components'))).toBe(true); expect(dependencies.has(_('/dist/shared'))).toBe(true); diff --git a/packages/compiler-cli/ngcc/test/packages/module_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts similarity index 99% rename from packages/compiler-cli/ngcc/test/packages/module_resolver_spec.ts rename to packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts index adc8ac33e7f2..550c66fcd3ac 100644 --- a/packages/compiler-cli/ngcc/test/packages/module_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts @@ -9,7 +9,7 @@ import * as mockFs from 'mock-fs'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {ModuleResolver, ResolvedDeepImport, ResolvedExternalModule, ResolvedRelativeModule} from '../../src/packages/module_resolver'; +import {ModuleResolver, ResolvedDeepImport, ResolvedExternalModule, ResolvedRelativeModule} from '../../src/dependencies/module_resolver'; const _ = AbsoluteFsPath.from; diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts index 15b615e87b95..e16eef9275a1 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts @@ -9,11 +9,11 @@ import * as mockFs from 'mock-fs'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {DependencyHost} from '../../src/packages/dependency_host'; -import {DependencyResolver} from '../../src/packages/dependency_resolver'; +import {DependencyResolver} from '../../src/dependencies/dependency_resolver'; +import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; +import {ModuleResolver} from '../../src/dependencies/module_resolver'; import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPointFinder} from '../../src/packages/entry_point_finder'; -import {ModuleResolver} from '../../src/packages/module_resolver'; import {MockLogger} from '../helpers/mock_logger'; const _ = AbsoluteFsPath.from; @@ -22,7 +22,8 @@ describe('findEntryPoints()', () => { let resolver: DependencyResolver; let finder: EntryPointFinder; beforeEach(() => { - resolver = new DependencyResolver(new MockLogger(), new DependencyHost(new ModuleResolver())); + resolver = + new DependencyResolver(new MockLogger(), new EsmDependencyHost(new ModuleResolver())); spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []}; }); From 4a6c35110b10d0fd0fc2914ccd64e82e9ede276f Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:57 +0100 Subject: [PATCH 10/13] refactor(ivy): ngcc - implement abstract FileSystem This commit introduces a new interface, which abstracts access to the underlying `FileSystem`. There is initially one concrete implementation, `NodeJsFileSystem`, which is simply wrapping the `fs` library of NodeJs. Going forward, we can provide a `MockFileSystem` for test, which should allow us to stop using `mock-fs` for most of the unit tests. We could also implement a `CachedFileSystem` that may improve the performance of ngcc. --- .../ngcc/src/analysis/decoration_analyzer.ts | 16 +-- .../analysis/private_declarations_analyzer.ts | 10 +- .../src/dependencies/esm_dependency_host.ts | 8 +- .../ngcc/src/dependencies/module_resolver.ts | 20 ++-- .../ngcc/src/file_system/file_system.ts | 38 ++++++ .../src/file_system/node_js_file_system.ts | 30 +++++ packages/compiler-cli/ngcc/src/main.ts | 56 +++++---- .../ngcc/src/packages/build_marker.ts | 8 +- .../ngcc/src/packages/bundle_program.ts | 30 ++--- .../ngcc/src/packages/entry_point.ts | 27 ++--- .../ngcc/src/packages/entry_point_bundle.ts | 24 ++-- .../ngcc/src/packages/entry_point_finder.ts | 38 +++--- .../ngcc/src/packages/ngcc_compiler_host.ts | 66 +++++++++++ .../ngcc/src/packages/transformer.ts | 11 +- .../ngcc/src/rendering/esm5_renderer.ts | 15 ++- .../ngcc/src/rendering/esm_renderer.ts | 22 ++-- .../ngcc/src/rendering/renderer.ts | 59 +++++----- .../ngcc/src/writing/in_place_file_writer.ts | 23 ++-- .../writing/new_entry_point_file_writer.ts | 39 ++++--- .../test/analysis/decoration_analyzer_spec.ts | 4 +- .../private_declarations_analyzer_spec.ts | 11 +- .../analysis/switch_marker_analyzer_spec.ts | 2 - .../dependencies/dependency_resolver_spec.ts | 4 +- .../dependencies/esm_dependency_host_spec.ts | 21 ++-- .../test/dependencies/module_resolver_spec.ts | 47 +++++--- .../compiler-cli/ngcc/test/helpers/utils.ts | 11 +- .../ngcc/test/host/esm2015_host_spec.ts | 12 +- .../ngcc/test/integration/ngcc_spec.ts | 7 +- .../ngcc/test/packages/build_marker_spec.ts | 13 ++- .../test/packages/entry_point_finder_spec.ts | 6 +- .../ngcc/test/packages/entry_point_spec.ts | 35 +++--- .../test/rendering/esm2015_renderer_spec.ts | 36 +++--- .../ngcc/test/rendering/esm5_renderer_spec.ts | 36 +++--- .../ngcc/test/rendering/renderer_spec.ts | 17 ++- .../test/writing/in_place_file_writer_spec.ts | 31 +++-- .../new_entry_point_file_writer_spec.ts | 108 ++++++++++++------ 36 files changed, 589 insertions(+), 352 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/file_system/file_system.ts create mode 100644 packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts create mode 100644 packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 65f491f0709f..b69cb2eeab50 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -6,9 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {ConstantPool} from '@angular/compiler'; -import {NOOP_PERF_RECORDER} from '@angular/compiler-cli/src/ngtsc/perf'; -import * as path from 'canonical-path'; -import * as fs from 'fs'; +import * as path from 'path'; import * as ts from 'typescript'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations'; @@ -19,6 +17,7 @@ import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {AbsoluteFsPath, LogicalFileSystem} from '../../../src/ngtsc/path'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope'; import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform'; +import {FileSystem} from '../file_system/file_system'; import {DecoratedClass} from '../host/decorated_class'; import {NgccReflectionHost} from '../host/ngcc_host'; import {isDefined} from '../utils'; @@ -53,9 +52,10 @@ export interface MatchingHandler { * Simple class that resolves and loads files directly from the filesystem. */ class NgccResourceLoader implements ResourceLoader { + constructor(private fs: FileSystem) {} canPreload = false; preload(): undefined|Promise { throw new Error('Not implemented.'); } - load(url: string): string { return fs.readFileSync(url, 'utf8'); } + load(url: string): string { return this.fs.readFile(AbsoluteFsPath.resolve(url)); } resolve(url: string, containingFile: string): string { return path.resolve(path.dirname(containingFile), url); } @@ -65,7 +65,7 @@ class NgccResourceLoader implements ResourceLoader { * This Analyzer will analyze the files that have decorated classes that need to be transformed. */ export class DecorationAnalyzer { - resourceManager = new NgccResourceLoader(); + resourceManager = new NgccResourceLoader(this.fs); metaRegistry = new LocalMetadataRegistry(); dtsMetaReader = new DtsMetadataReader(this.typeChecker, this.reflectionHost); fullMetaReader = new CompoundMetadataReader([this.metaRegistry, this.dtsMetaReader]); @@ -73,8 +73,8 @@ export class DecorationAnalyzer { new LocalIdentifierStrategy(), new AbsoluteModuleStrategy(this.program, this.typeChecker, this.options, this.host), // TODO(alxhub): there's no reason why ngcc needs the "logical file system" logic here, as ngcc - // projects only ever have one rootDir. Instead, ngcc should just switch its emitted imort based - // on whether a bestGuessOwningModule is present in the Reference. + // projects only ever have one rootDir. Instead, ngcc should just switch its emitted import + // based on whether a bestGuessOwningModule is present in the Reference. new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)), ]); dtsModuleScopeResolver = @@ -110,7 +110,7 @@ export class DecorationAnalyzer { ]; constructor( - private program: ts.Program, private options: ts.CompilerOptions, + private fs: FileSystem, private program: ts.Program, private options: ts.CompilerOptions, private host: ts.CompilerHost, private typeChecker: ts.TypeChecker, private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry, private rootDirs: AbsoluteFsPath[], private isCore: boolean) {} diff --git a/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts index ff7d3ce2ccc1..72698b55824c 100644 --- a/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts @@ -7,6 +7,7 @@ */ import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {Declaration} from '../../../src/ngtsc/reflection'; import {NgccReflectionHost} from '../host/ngcc_host'; import {hasNameIdentifier, isDefined} from '../utils'; @@ -14,8 +15,8 @@ import {NgccReferencesRegistry} from './ngcc_references_registry'; export interface ExportInfo { identifier: string; - from: string; - dtsFrom?: string|null; + from: AbsoluteFsPath; + dtsFrom?: AbsoluteFsPath|null; alias?: string|null; } export type PrivateDeclarationsAnalyses = ExportInfo[]; @@ -93,11 +94,12 @@ export class PrivateDeclarationsAnalyzer { }); return Array.from(privateDeclarations.keys()).map(id => { - const from = id.getSourceFile().fileName; + const from = AbsoluteFsPath.fromSourceFile(id.getSourceFile()); const declaration = privateDeclarations.get(id) !; const alias = exportAliasDeclarations.get(id) || null; const dtsDeclaration = this.host.getDtsDeclaration(declaration.node); - const dtsFrom = dtsDeclaration && dtsDeclaration.getSourceFile().fileName; + const dtsFrom = + dtsDeclaration && AbsoluteFsPath.fromSourceFile(dtsDeclaration.getSourceFile()); return {identifier: id.text, from, dtsFrom, alias}; }); diff --git a/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts index b71663f16741..33c763069870 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts @@ -5,12 +5,10 @@ * 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 fs from 'fs'; import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; - +import {FileSystem} from '../file_system/file_system'; import {DependencyHost, DependencyInfo} from './dependency_host'; import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; @@ -19,7 +17,7 @@ import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './modu * Helper functions for computing dependencies. */ export class EsmDependencyHost implements DependencyHost { - constructor(private moduleResolver: ModuleResolver) {} + constructor(private fs: FileSystem, private moduleResolver: ModuleResolver) {} /** * Find all the dependencies for the entry-point at the given path. @@ -54,7 +52,7 @@ export class EsmDependencyHost implements DependencyHost { private recursivelyFindDependencies( file: AbsoluteFsPath, dependencies: Set, missing: Set, deepImports: Set, alreadySeen: Set): void { - const fromContents = fs.readFileSync(file, 'utf8'); + const fromContents = this.fs.readFile(file); if (!this.hasImportOrReexportStatements(fromContents)) { return; } diff --git a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts index 1f3e4e704b13..e8c12e361da6 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts @@ -6,12 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import * as fs from 'fs'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; import {PathMappings, isRelativePath} from '../utils'; - /** * This is a very cut-down implementation of the TypeScript module resolution strategy. * @@ -28,7 +26,9 @@ import {PathMappings, isRelativePath} from '../utils'; export class ModuleResolver { private pathMappings: ProcessedPathMapping[]; - constructor(pathMappings?: PathMappings, private relativeExtensions = ['.js', '/index.js']) { + constructor(private fs: FileSystem, pathMappings?: PathMappings, private relativeExtensions = [ + '.js', '/index.js' + ]) { this.pathMappings = pathMappings ? this.processPathMappings(pathMappings) : []; } @@ -139,11 +139,11 @@ export class ModuleResolver { * to the `path` and checking if the file exists on disk. * @returns An absolute path to the first matching existing file, or `null` if none exist. */ - private resolvePath(path: string, postFixes: string[]): AbsoluteFsPath|null { + private resolvePath(path: AbsoluteFsPath, postFixes: string[]): AbsoluteFsPath|null { for (const postFix of postFixes) { - const testPath = path + postFix; - if (fs.existsSync(testPath)) { - return AbsoluteFsPath.from(testPath); + const testPath = AbsoluteFsPath.fromUnchecked(path + postFix); + if (this.fs.exists(testPath)) { + return testPath; } } return null; @@ -155,7 +155,7 @@ export class ModuleResolver { * This is achieved by checking for the existence of `${modulePath}/package.json`. */ private isEntryPoint(modulePath: AbsoluteFsPath): boolean { - return fs.existsSync(AbsoluteFsPath.join(modulePath, 'package.json')); + return this.fs.exists(AbsoluteFsPath.join(modulePath, 'package.json')); } /** @@ -227,7 +227,7 @@ export class ModuleResolver { let folder = path; while (folder !== '/') { folder = AbsoluteFsPath.dirname(folder); - if (fs.existsSync(AbsoluteFsPath.join(folder, 'package.json'))) { + if (this.fs.exists(AbsoluteFsPath.join(folder, 'package.json'))) { return folder; } } diff --git a/packages/compiler-cli/ngcc/src/file_system/file_system.ts b/packages/compiler-cli/ngcc/src/file_system/file_system.ts new file mode 100644 index 000000000000..9c9653b48b71 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/file_system/file_system.ts @@ -0,0 +1,38 @@ +/** + * @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 {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; + +/** + * A basic interface to abstract the underlying file-system. + * + * This makes it easier to provide mock file-systems in unit tests, + * but also to create clever file-systems that have features such as caching. + */ +export interface FileSystem { + exists(path: AbsoluteFsPath): boolean; + readFile(path: AbsoluteFsPath): string; + writeFile(path: AbsoluteFsPath, data: string): void; + readdir(path: AbsoluteFsPath): PathSegment[]; + lstat(path: AbsoluteFsPath): FileStats; + stat(path: AbsoluteFsPath): FileStats; + pwd(): AbsoluteFsPath; + copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void; + moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void; + ensureDir(path: AbsoluteFsPath): void; +} + +/** + * Information about an object in the FileSystem. + * This is analogous to the `fs.Stats` class in Node.js. + */ +export interface FileStats { + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; +} diff --git a/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts b/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts new file mode 100644 index 000000000000..7f821c398669 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts @@ -0,0 +1,30 @@ +/** + * @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 fs from 'fs'; +import {cp, mkdir, mv} from 'shelljs'; +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {FileSystem} from './file_system'; + +/** + * A wrapper around the Node.js file-system (i.e the `fs` package). + */ +export class NodeJSFileSystem implements FileSystem { + exists(path: AbsoluteFsPath): boolean { return fs.existsSync(path); } + readFile(path: AbsoluteFsPath): string { return fs.readFileSync(path, 'utf8'); } + writeFile(path: AbsoluteFsPath, data: string): void { + return fs.writeFileSync(path, data, 'utf8'); + } + readdir(path: AbsoluteFsPath): PathSegment[] { return fs.readdirSync(path) as PathSegment[]; } + lstat(path: AbsoluteFsPath): fs.Stats { return fs.lstatSync(path); } + stat(path: AbsoluteFsPath): fs.Stats { return fs.statSync(path); } + pwd() { return AbsoluteFsPath.fromUnchecked(process.cwd()); } + copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { cp(from, to); } + moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { mv(from, to); } + ensureDir(path: AbsoluteFsPath): void { mkdir('-p', path); } +} diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 8ed2a2faf2d6..c69424fa9144 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -5,15 +5,13 @@ * 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 {readFileSync} from 'fs'; - import {AbsoluteFsPath} from '../../src/ngtsc/path'; import {DependencyResolver} from './dependencies/dependency_resolver'; import {EsmDependencyHost} from './dependencies/esm_dependency_host'; import {ModuleResolver} from './dependencies/module_resolver'; +import {FileSystem} from './file_system/file_system'; +import {NodeJSFileSystem} from './file_system/node_js_file_system'; import {ConsoleLogger, LogLevel} from './logging/console_logger'; import {Logger} from './logging/logger'; import {hasBeenProcessed, markAsProcessed} from './packages/build_marker'; @@ -26,8 +24,6 @@ import {FileWriter} from './writing/file_writer'; import {InPlaceFileWriter} from './writing/in_place_file_writer'; import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer'; - - /** * The options to configure the ngcc compiler. */ @@ -80,20 +76,20 @@ export function mainNgcc( {basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, compileAllFormats = true, createNewEntryPointFormats = false, logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void { - const transformer = new Transformer(logger); - const moduleResolver = new ModuleResolver(pathMappings); - const host = new EsmDependencyHost(moduleResolver); + const fs = new NodeJSFileSystem(); + const transformer = new Transformer(fs, logger); + const moduleResolver = new ModuleResolver(fs, pathMappings); + const host = new EsmDependencyHost(fs, moduleResolver); const resolver = new DependencyResolver(logger, host); - const finder = new EntryPointFinder(logger, resolver); - const fileWriter = getFileWriter(createNewEntryPointFormats); + const finder = new EntryPointFinder(fs, logger, resolver); + const fileWriter = getFileWriter(fs, createNewEntryPointFormats); - const absoluteTargetEntryPointPath = targetEntryPointPath ? - AbsoluteFsPath.from(resolve(basePath, targetEntryPointPath)) : - undefined; + const absoluteTargetEntryPointPath = + targetEntryPointPath ? AbsoluteFsPath.resolve(basePath, targetEntryPointPath) : undefined; if (absoluteTargetEntryPointPath && hasProcessedTargetEntryPoint( - absoluteTargetEntryPointPath, propertiesToConsider, compileAllFormats)) { + fs, absoluteTargetEntryPointPath, propertiesToConsider, compileAllFormats)) { logger.info('The target entry-point has already been processed'); return; } @@ -102,7 +98,7 @@ export function mainNgcc( AbsoluteFsPath.from(basePath), absoluteTargetEntryPointPath, pathMappings); if (absoluteTargetEntryPointPath && entryPoints.length === 0) { - markNonAngularPackageAsProcessed(absoluteTargetEntryPointPath, propertiesToConsider); + markNonAngularPackageAsProcessed(fs, absoluteTargetEntryPointPath, propertiesToConsider); return; } @@ -138,8 +134,8 @@ export function mainNgcc( // the property as processed even if its underlying format has been built already. if (!compiledFormats.has(formatPath) && (compileAllFormats || isFirstFormat)) { const bundle = makeEntryPointBundle( - entryPoint.path, formatPath, entryPoint.typings, isCore, property, format, processDts, - pathMappings); + fs, entryPoint.path, formatPath, entryPoint.typings, isCore, property, format, + processDts, pathMappings); if (bundle) { logger.info(`Compiling ${entryPoint.name} : ${property} as ${format}`); const transformedFiles = transformer.transform(bundle); @@ -156,9 +152,9 @@ export function mainNgcc( // Either this format was just compiled or its underlying format was compiled because of a // previous property. if (compiledFormats.has(formatPath)) { - markAsProcessed(entryPointPackageJson, entryPointPackageJsonPath, property); + markAsProcessed(fs, entryPointPackageJson, entryPointPackageJsonPath, property); if (processDts) { - markAsProcessed(entryPointPackageJson, entryPointPackageJsonPath, 'typings'); + markAsProcessed(fs, entryPointPackageJson, entryPointPackageJsonPath, 'typings'); } } } @@ -170,14 +166,15 @@ export function mainNgcc( }); } -function getFileWriter(createNewEntryPointFormats: boolean): FileWriter { - return createNewEntryPointFormats ? new NewEntryPointFileWriter() : new InPlaceFileWriter(); +function getFileWriter(fs: FileSystem, createNewEntryPointFormats: boolean): FileWriter { + return createNewEntryPointFormats ? new NewEntryPointFileWriter(fs) : new InPlaceFileWriter(fs); } function hasProcessedTargetEntryPoint( - targetPath: AbsoluteFsPath, propertiesToConsider: string[], compileAllFormats: boolean) { - const packageJsonPath = AbsoluteFsPath.from(resolve(targetPath, 'package.json')); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + fs: FileSystem, targetPath: AbsoluteFsPath, propertiesToConsider: string[], + compileAllFormats: boolean) { + const packageJsonPath = AbsoluteFsPath.resolve(targetPath, 'package.json'); + const packageJson = JSON.parse(fs.readFile(packageJsonPath)); for (const property of propertiesToConsider) { if (packageJson[property]) { @@ -205,11 +202,12 @@ function hasProcessedTargetEntryPoint( * So mark all formats in this entry-point as processed so that clients of ngcc can avoid * triggering ngcc for this entry-point in the future. */ -function markNonAngularPackageAsProcessed(path: AbsoluteFsPath, propertiesToConsider: string[]) { - const packageJsonPath = AbsoluteFsPath.from(resolve(path, 'package.json')); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); +function markNonAngularPackageAsProcessed( + fs: FileSystem, path: AbsoluteFsPath, propertiesToConsider: string[]) { + const packageJsonPath = AbsoluteFsPath.resolve(path, 'package.json'); + const packageJson = JSON.parse(fs.readFile(packageJsonPath)); propertiesToConsider.forEach(formatProperty => { if (packageJson[formatProperty]) - markAsProcessed(packageJson, packageJsonPath, formatProperty as EntryPointJsonProperty); + markAsProcessed(fs, packageJson, packageJsonPath, formatProperty as EntryPointJsonProperty); }); } diff --git a/packages/compiler-cli/ngcc/src/packages/build_marker.ts b/packages/compiler-cli/ngcc/src/packages/build_marker.ts index 5aea60f51c6f..c55dbd39c137 100644 --- a/packages/compiler-cli/ngcc/src/packages/build_marker.ts +++ b/packages/compiler-cli/ngcc/src/packages/build_marker.ts @@ -6,10 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {writeFileSync} from 'fs'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; - +import {FileSystem} from '../file_system/file_system'; import {EntryPointJsonProperty, EntryPointPackageJson} from './entry_point'; export const NGCC_VERSION = '0.0.0-PLACEHOLDER'; @@ -49,9 +47,9 @@ export function hasBeenProcessed( * @param format the property in the package.json of the format for which we are writing the marker. */ export function markAsProcessed( - packageJson: EntryPointPackageJson, packageJsonPath: AbsoluteFsPath, + fs: FileSystem, packageJson: EntryPointPackageJson, packageJsonPath: AbsoluteFsPath, format: EntryPointJsonProperty) { if (!packageJson.__processed_by_ivy_ngcc__) packageJson.__processed_by_ivy_ngcc__ = {}; packageJson.__processed_by_ivy_ngcc__[format] = NGCC_VERSION; - writeFileSync(packageJsonPath, JSON.stringify(packageJson), 'utf8'); + fs.writeFile(packageJsonPath, JSON.stringify(packageJson)); } diff --git a/packages/compiler-cli/ngcc/src/packages/bundle_program.ts b/packages/compiler-cli/ngcc/src/packages/bundle_program.ts index 5c8ba5eeb3bc..97ba48e2655d 100644 --- a/packages/compiler-cli/ngcc/src/packages/bundle_program.ts +++ b/packages/compiler-cli/ngcc/src/packages/bundle_program.ts @@ -5,10 +5,11 @@ * 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 {dirname, resolve} from 'canonical-path'; -import {existsSync, lstatSync, readdirSync} from 'fs'; import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; + /** * An entry point bundle contains one or two programs, e.g. `src` and `dts`, * that are compiled via TypeScript. @@ -21,9 +22,9 @@ export interface BundleProgram { program: ts.Program; options: ts.CompilerOptions; host: ts.CompilerHost; - path: string; + path: AbsoluteFsPath; file: ts.SourceFile; - r3SymbolsPath: string|null; + r3SymbolsPath: AbsoluteFsPath|null; r3SymbolsFile: ts.SourceFile|null; } @@ -31,9 +32,10 @@ export interface BundleProgram { * Create a bundle program. */ export function makeBundleProgram( - isCore: boolean, path: string, r3FileName: string, options: ts.CompilerOptions, - host: ts.CompilerHost): BundleProgram { - const r3SymbolsPath = isCore ? findR3SymbolsPath(dirname(path), r3FileName) : null; + fs: FileSystem, isCore: boolean, path: AbsoluteFsPath, r3FileName: string, + options: ts.CompilerOptions, host: ts.CompilerHost): BundleProgram { + const r3SymbolsPath = + isCore ? findR3SymbolsPath(fs, AbsoluteFsPath.dirname(path), r3FileName) : null; const rootPaths = r3SymbolsPath ? [path, r3SymbolsPath] : [path]; const program = ts.createProgram(rootPaths, options, host); const file = program.getSourceFile(path) !; @@ -45,26 +47,28 @@ export function makeBundleProgram( /** * Search the given directory hierarchy to find the path to the `r3_symbols` file. */ -export function findR3SymbolsPath(directory: string, filename: string): string|null { - const r3SymbolsFilePath = resolve(directory, filename); - if (existsSync(r3SymbolsFilePath)) { +export function findR3SymbolsPath( + fs: FileSystem, directory: AbsoluteFsPath, filename: string): AbsoluteFsPath|null { + const r3SymbolsFilePath = AbsoluteFsPath.resolve(directory, filename); + if (fs.exists(r3SymbolsFilePath)) { return r3SymbolsFilePath; } const subDirectories = - readdirSync(directory) + fs.readdir(directory) // 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 = lstatSync(resolve(directory, p)); + const stat = fs.lstat(AbsoluteFsPath.resolve(directory, p)); return stat.isDirectory() && !stat.isSymbolicLink(); }); for (const subDirectory of subDirectories) { - const r3SymbolsFilePath = findR3SymbolsPath(resolve(directory, subDirectory, ), filename); + const r3SymbolsFilePath = + findR3SymbolsPath(fs, AbsoluteFsPath.resolve(directory, subDirectory), filename); if (r3SymbolsFilePath) { return r3SymbolsFilePath; } diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/ngcc/src/packages/entry_point.ts index 5f1d92fe26d3..8a8c8bd9bc49 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point.ts @@ -5,14 +5,10 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; import {Logger} from '../logging/logger'; - /** * The possible values for the format of an entry-point. */ @@ -70,13 +66,14 @@ export const SUPPORTED_FORMAT_PROPERTIES: EntryPointJsonProperty[] = * @returns An entry-point if it is valid, `null` otherwise. */ export function getEntryPointInfo( - logger: Logger, packagePath: AbsoluteFsPath, entryPointPath: AbsoluteFsPath): EntryPoint|null { - const packageJsonPath = path.resolve(entryPointPath, 'package.json'); - if (!fs.existsSync(packageJsonPath)) { + fs: FileSystem, logger: Logger, packagePath: AbsoluteFsPath, + entryPointPath: AbsoluteFsPath): EntryPoint|null { + const packageJsonPath = AbsoluteFsPath.resolve(entryPointPath, 'package.json'); + if (!fs.exists(packageJsonPath)) { return null; } - const entryPointPackageJson = loadEntryPointPackage(logger, packageJsonPath); + const entryPointPackageJson = loadEntryPointPackage(fs, logger, packageJsonPath); if (!entryPointPackageJson) { return null; } @@ -90,15 +87,15 @@ export function getEntryPointInfo( // Also there must exist a `metadata.json` file next to the typings entry-point. const metadataPath = - path.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json'); + AbsoluteFsPath.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json'); const entryPointInfo: EntryPoint = { name: entryPointPackageJson.name, packageJson: entryPointPackageJson, package: packagePath, path: entryPointPath, - typings: AbsoluteFsPath.from(path.resolve(entryPointPath, typings)), - compiledByAngular: fs.existsSync(metadataPath), + typings: AbsoluteFsPath.resolve(entryPointPath, typings), + compiledByAngular: fs.exists(metadataPath), }; return entryPointInfo; @@ -136,10 +133,10 @@ export function getEntryPointFormat(property: string): EntryPointFormat|undefine * @param packageJsonPath the absolute path to the package.json file. * @returns JSON from the package.json file if it is valid, `null` otherwise. */ -function loadEntryPointPackage(logger: Logger, packageJsonPath: string): EntryPointPackageJson| - null { +function loadEntryPointPackage( + fs: FileSystem, logger: Logger, packageJsonPath: AbsoluteFsPath): EntryPointPackageJson|null { try { - return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + return JSON.parse(fs.readFile(packageJsonPath)); } catch (e) { // We may have run into a package.json with unexpected symbols logger.warn(`Failed to read entry point info from ${packageJsonPath} with error ${e}.`); diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts index 778577e30529..d61f12341b29 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts @@ -5,14 +5,14 @@ * 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 * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; import {PathMappings} from '../utils'; - import {BundleProgram, makeBundleProgram} from './bundle_program'; import {EntryPointFormat, EntryPointJsonProperty} from './entry_point'; +import {NgccCompilerHost} from './ngcc_compiler_host'; /** * A bundle of files and paths (and TS programs) that correspond to a particular @@ -38,25 +38,27 @@ export interface EntryPointBundle { * @param transformDts Whether to transform the typings along with this bundle. */ export function makeEntryPointBundle( - entryPointPath: string, formatPath: string, typingsPath: string, isCore: boolean, - formatProperty: EntryPointJsonProperty, format: EntryPointFormat, transformDts: boolean, - pathMappings?: PathMappings): EntryPointBundle|null { + fs: FileSystem, entryPointPath: string, formatPath: string, typingsPath: string, + isCore: boolean, formatProperty: EntryPointJsonProperty, format: EntryPointFormat, + transformDts: boolean, pathMappings?: PathMappings): EntryPointBundle|null { // Create the TS program and necessary helpers. const options: ts.CompilerOptions = { allowJs: true, maxNodeModuleJsDepth: Infinity, + noLib: true, rootDir: entryPointPath, ...pathMappings }; - const host = ts.createCompilerHost(options); + const host = new NgccCompilerHost(fs, options); const rootDirs = [AbsoluteFsPath.from(entryPointPath)]; // Create the bundle programs, as necessary. const src = makeBundleProgram( - isCore, resolve(entryPointPath, formatPath), 'r3_symbols.js', options, host); - const dts = transformDts ? - makeBundleProgram( - isCore, resolve(entryPointPath, typingsPath), 'r3_symbols.d.ts', options, host) : - null; + fs, isCore, AbsoluteFsPath.resolve(entryPointPath, formatPath), 'r3_symbols.js', options, + host); + const dts = transformDts ? makeBundleProgram( + fs, isCore, AbsoluteFsPath.resolve(entryPointPath, typingsPath), + 'r3_symbols.d.ts', options, host) : + null; const isFlatCore = isCore && src.r3SymbolsFile === null; return {format, formatProperty, rootDirs, isCore, isFlatCore, src, dts}; diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts index c52c0cbba113..f739669480a3 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts @@ -5,20 +5,16 @@ * 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 {join, resolve} from 'path'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DependencyResolver, SortedEntryPointsInfo} from '../dependencies/dependency_resolver'; +import {FileSystem} from '../file_system/file_system'; import {Logger} from '../logging/logger'; import {PathMappings} from '../utils'; - import {EntryPoint, getEntryPointInfo} from './entry_point'; - export class EntryPointFinder { - constructor(private logger: Logger, private resolver: DependencyResolver) {} + constructor( + private fs: FileSystem, private logger: Logger, 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. @@ -59,9 +55,9 @@ export class EntryPointFinder { AbsoluteFsPath[] { const basePaths = [sourceDirectory]; if (pathMappings) { - const baseUrl = AbsoluteFsPath.from(resolve(pathMappings.baseUrl)); + const baseUrl = AbsoluteFsPath.resolve(pathMappings.baseUrl); values(pathMappings.paths).forEach(paths => paths.forEach(path => { - basePaths.push(AbsoluteFsPath.fromUnchecked(join(baseUrl, extractPathPrefix(path)))); + basePaths.push(AbsoluteFsPath.join(baseUrl, extractPathPrefix(path))); })); } basePaths.sort(); // Get the paths in order with the shorter ones first. @@ -75,29 +71,29 @@ export class EntryPointFinder { */ private walkDirectoryForEntryPoints(sourceDirectory: AbsoluteFsPath): EntryPoint[] { const entryPoints: EntryPoint[] = []; - fs.readdirSync(sourceDirectory) + this.fs + .readdir(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)); + const stat = this.fs.lstat(AbsoluteFsPath.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 = AbsoluteFsPath.from(path.join(sourceDirectory, p)); + const packagePath = AbsoluteFsPath.join(sourceDirectory, p); if (p.startsWith('@')) { entryPoints.push(...this.walkDirectoryForEntryPoints(packagePath)); } else { entryPoints.push(...this.getEntryPointsForPackage(packagePath)); // Also check for any nested node_modules in this package - const nestedNodeModulesPath = - AbsoluteFsPath.from(path.resolve(packagePath, 'node_modules')); - if (fs.existsSync(nestedNodeModulesPath)) { + const nestedNodeModulesPath = AbsoluteFsPath.resolve(packagePath, 'node_modules'); + if (this.fs.exists(nestedNodeModulesPath)) { entryPoints.push(...this.walkDirectoryForEntryPoints(nestedNodeModulesPath)); } } @@ -114,14 +110,14 @@ export class EntryPointFinder { const entryPoints: EntryPoint[] = []; // Try to get an entry point from the top level package directory - const topLevelEntryPoint = getEntryPointInfo(this.logger, packagePath, packagePath); + const topLevelEntryPoint = getEntryPointInfo(this.fs, this.logger, packagePath, packagePath); if (topLevelEntryPoint !== null) { entryPoints.push(topLevelEntryPoint); } // Now search all the directories of this package for possible entry points this.walkDirectory(packagePath, subdir => { - const subEntryPoint = getEntryPointInfo(this.logger, packagePath, subdir); + const subEntryPoint = getEntryPointInfo(this.fs, this.logger, packagePath, subdir); if (subEntryPoint !== null) { entryPoints.push(subEntryPoint); } @@ -137,19 +133,19 @@ export class EntryPointFinder { * @param fn the function to apply to each directory. */ private walkDirectory(dir: AbsoluteFsPath, fn: (dir: AbsoluteFsPath) => void) { - return fs - .readdirSync(dir) + return this.fs + .readdir(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)); + const stat = this.fs.lstat(AbsoluteFsPath.resolve(dir, p)); return stat.isDirectory() && !stat.isSymbolicLink(); }) .forEach(subDir => { - const resolvedSubDir = AbsoluteFsPath.from(path.resolve(dir, subDir)); + const resolvedSubDir = AbsoluteFsPath.resolve(dir, subDir); fn(resolvedSubDir); this.walkDirectory(resolvedSubDir, fn); }); diff --git a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts new file mode 100644 index 000000000000..8f8d5cd597c4 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts @@ -0,0 +1,66 @@ +/** + * @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 os from 'os'; +import * as ts from 'typescript'; + +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; + +export class NgccCompilerHost implements ts.CompilerHost { + private _caseSensitive = this.fs.exists(AbsoluteFsPath.fromUnchecked(__filename.toUpperCase())); + + constructor(private fs: FileSystem, private options: ts.CompilerOptions) {} + + getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined { + const text = this.readFile(fileName); + return text !== undefined ? ts.createSourceFile(fileName, text, languageVersion) : undefined; + } + + getDefaultLibFileName(options: ts.CompilerOptions): string { + return this.getDefaultLibLocation() + '/' + ts.getDefaultLibFileName(options); + } + + getDefaultLibLocation(): string { + const nodeLibPath = AbsoluteFsPath.fromUnchecked(require.resolve('typescript')); + return AbsoluteFsPath.join(nodeLibPath, '..'); + } + + writeFile(fileName: string, data: string): void { + this.fs.writeFile(AbsoluteFsPath.fromUnchecked(fileName), data); + } + + getCurrentDirectory(): string { return this.fs.pwd(); } + + getCanonicalFileName(fileName: string): string { + return this.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); + } + + useCaseSensitiveFileNames(): boolean { return this._caseSensitive; } + + getNewLine(): string { + switch (this.options.newLine) { + case ts.NewLineKind.CarriageReturnLineFeed: + return '\r\n'; + case ts.NewLineKind.LineFeed: + return '\n'; + default: + return os.EOL; + } + } + + fileExists(fileName: string): boolean { + return this.fs.exists(AbsoluteFsPath.fromUnchecked(fileName)); + } + + readFile(fileName: string): string|undefined { + if (!this.fileExists(fileName)) { + return undefined; + } + return this.fs.readFile(AbsoluteFsPath.fromUnchecked(fileName)); + } +} diff --git a/packages/compiler-cli/ngcc/src/packages/transformer.ts b/packages/compiler-cli/ngcc/src/packages/transformer.ts index 3ca19262bff6..3abc7078a3a0 100644 --- a/packages/compiler-cli/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/ngcc/src/packages/transformer.ts @@ -12,6 +12,7 @@ import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../analy import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry'; import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer'; +import {FileSystem} from '../file_system/file_system'; import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm5ReflectionHost} from '../host/esm5_host'; import {NgccReflectionHost} from '../host/ngcc_host'; @@ -46,7 +47,7 @@ import {EntryPointBundle} from './entry_point_bundle'; * - Some formats may contain multiple "modules" in a single file. */ export class Transformer { - constructor(private logger: Logger) {} + constructor(private fs: FileSystem, private logger: Logger) {} /** * Transform the source (and typings) files of a bundle. @@ -85,9 +86,9 @@ export class Transformer { getRenderer(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): Renderer { switch (bundle.format) { case 'esm2015': - return new EsmRenderer(this.logger, host, isCore, bundle); + return new EsmRenderer(this.fs, this.logger, host, isCore, bundle); case 'esm5': - return new Esm5Renderer(this.logger, host, isCore, bundle); + return new Esm5Renderer(this.fs, this.logger, host, isCore, bundle); default: throw new Error(`Renderer for "${bundle.format}" not yet implemented.`); } @@ -102,8 +103,8 @@ export class Transformer { const switchMarkerAnalyses = switchMarkerAnalyzer.analyzeProgram(bundle.src.program); const decorationAnalyzer = new DecorationAnalyzer( - bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, reflectionHost, - referencesRegistry, bundle.rootDirs, isCore); + this.fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, + reflectionHost, referencesRegistry, bundle.rootDirs, isCore); const decorationAnalyses = decorationAnalyzer.analyzeProgram(); const moduleWithProvidersAnalyzer = diff --git a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts index 7158b250a15a..368c93b44f91 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts @@ -5,18 +5,21 @@ * 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 ts from 'typescript'; import MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {CompiledClass} from '../analysis/decoration_analyzer'; +import {FileSystem} from '../file_system/file_system'; import {getIifeBody} from '../host/esm5_host'; import {NgccReflectionHost} from '../host/ngcc_host'; -import {CompiledClass} from '../analysis/decoration_analyzer'; -import {EsmRenderer} from './esm_renderer'; -import {EntryPointBundle} from '../packages/entry_point_bundle'; import {Logger} from '../logging/logger'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {EsmRenderer} from './esm_renderer'; export class Esm5Renderer extends EsmRenderer { - constructor(logger: Logger, host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle) { - super(logger, host, isCore, bundle); + constructor( + fs: FileSystem, logger: Logger, host: NgccReflectionHost, isCore: boolean, + bundle: EntryPointBundle) { + super(fs, logger, host, isCore, bundle); } /** diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts index 04a3b928f969..15fe6f61fbee 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts @@ -5,20 +5,23 @@ * 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 {dirname, relative} from 'canonical-path'; import MagicString from 'magic-string'; import * as ts from 'typescript'; -import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; +import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {CompiledClass} from '../analysis/decoration_analyzer'; -import {RedundantDecoratorMap, Renderer, stripExtension} from './renderer'; -import {EntryPointBundle} from '../packages/entry_point_bundle'; import {ExportInfo} from '../analysis/private_declarations_analyzer'; -import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; +import {FileSystem} from '../file_system/file_system'; +import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; import {Logger} from '../logging/logger'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {RedundantDecoratorMap, Renderer, stripExtension} from './renderer'; export class EsmRenderer extends Renderer { - constructor(logger: Logger, host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle) { - super(logger, host, isCore, bundle); + constructor( + fs: FileSystem, logger: Logger, host: NgccReflectionHost, isCore: boolean, + bundle: EntryPointBundle) { + super(fs, logger, host, isCore, bundle); } /** @@ -33,7 +36,7 @@ export class EsmRenderer extends Renderer { output.appendLeft(insertionPoint, renderedImports); } - addExports(output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void { + addExports(output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[]): void { exports.forEach(e => { let exportFrom = ''; const isDtsFile = isDtsPath(entryPointBasePath); @@ -41,7 +44,8 @@ export class EsmRenderer extends Renderer { if (from) { const basePath = stripExtension(from); - const relativePath = './' + relative(dirname(entryPointBasePath), basePath); + const relativePath = + './' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath); exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : ''; } diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index b6016a2ce6d5..911a8aac7596 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -6,25 +6,26 @@ * found in the LICENSE file at https://angular.io/license */ import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler'; -import {SourceMapConverter, commentRegex, fromJSON, fromMapFileSource, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map'; -import {readFileSync, statSync} from 'fs'; +import {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map'; import MagicString from 'magic-string'; -import {basename, dirname, relative, resolve} from 'canonical-path'; import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map'; import * as ts from 'typescript'; -import {NoopImportRewriter, ImportRewriter, R3SymbolsImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER} from '@angular/compiler-cli/src/ngtsc/imports'; -import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform'; +import {NoopImportRewriter, ImportRewriter, R3SymbolsImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports'; +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {CompileResult} from '../../../src/ngtsc/transform'; import {translateStatement, translateType, ImportManager} from '../../../src/ngtsc/translator'; -import {NgccFlatImportRewriter} from './ngcc_import_rewriter'; + import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer'; import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer'; import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer'; import {IMPORT_PREFIX} from '../constants'; +import {FileSystem} from '../file_system/file_system'; import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host'; -import {EntryPointBundle} from '../packages/entry_point_bundle'; import {Logger} from '../logging/logger'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {NgccFlatImportRewriter} from './ngcc_import_rewriter'; interface SourceMapInfo { source: string; @@ -39,7 +40,7 @@ export interface FileInfo { /** * Path to where the file should be written. */ - path: string; + path: AbsoluteFsPath; /** * The contents of the file to be be written. */ @@ -81,8 +82,8 @@ export const RedundantDecoratorMap = Map; */ export abstract class Renderer { constructor( - protected logger: Logger, protected host: NgccReflectionHost, protected isCore: boolean, - protected bundle: EntryPointBundle) {} + protected fs: FileSystem, protected logger: Logger, protected host: NgccReflectionHost, + protected isCore: boolean, protected bundle: EntryPointBundle) {} renderProgram( decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses, @@ -189,7 +190,7 @@ export abstract class Renderer { this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager); this.addImports(outputText, importManager.getAllImports(dtsFile.fileName), dtsFile); - this.addExports(outputText, dtsFile.fileName, renderInfo.privateExports); + this.addExports(outputText, AbsoluteFsPath.fromSourceFile(dtsFile), renderInfo.privateExports); return this.renderSourceAndMap(dtsFile, input, outputText); @@ -207,11 +208,12 @@ export abstract class Renderer { importManager: ImportManager): void { moduleWithProviders.forEach(info => { const ngModuleName = info.ngModule.node.name.text; - const declarationFile = info.declaration.getSourceFile().fileName; - const ngModuleFile = info.ngModule.node.getSourceFile().fileName; + const declarationFile = AbsoluteFsPath.fromSourceFile(info.declaration.getSourceFile()); + const ngModuleFile = AbsoluteFsPath.fromSourceFile(info.ngModule.node.getSourceFile()); const importPath = info.ngModule.viaModule || (declarationFile !== ngModuleFile ? - stripExtension(`./${relative(dirname(declarationFile), ngModuleFile)}`) : + stripExtension( + `./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) : null); const ngModule = getImportString(importManager, importPath, ngModuleName); @@ -252,7 +254,7 @@ export abstract class Renderer { output: MagicString, imports: {specifier: string, qualifier: string}[], sf: ts.SourceFile): void; protected abstract addExports( - output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void; + output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[]): void; protected abstract addDefinitions( output: MagicString, compiledClass: CompiledClass, definitions: string): void; protected abstract removeDecorators( @@ -288,7 +290,7 @@ export abstract class Renderer { */ protected extractSourceMap(file: ts.SourceFile): SourceMapInfo { const inline = commentRegex.test(file.text); - const external = mapFileCommentRegex.test(file.text); + const external = mapFileCommentRegex.exec(file.text); if (inline) { const inlineSourceMap = fromSource(file.text); @@ -300,17 +302,22 @@ export abstract class Renderer { } else if (external) { let externalSourceMap: SourceMapConverter|null = null; try { - externalSourceMap = fromMapFileSource(file.text, dirname(file.fileName)); + const fileName = external[1] || external[2]; + const filePath = AbsoluteFsPath.resolve( + AbsoluteFsPath.dirname(AbsoluteFsPath.fromSourceFile(file)), fileName); + const mappingFile = this.fs.readFile(filePath); + externalSourceMap = fromJSON(mappingFile); } catch (e) { if (e.code === 'ENOENT') { this.logger.warn( `The external map file specified in the source code comment "${e.path}" was not found on the file system.`); - const mapPath = file.fileName + '.map'; - if (basename(e.path) !== basename(mapPath) && statSync(mapPath).isFile()) { + const mapPath = AbsoluteFsPath.fromUnchecked(file.fileName + '.map'); + if (PathSegment.basename(e.path) !== PathSegment.basename(mapPath) && + this.fs.stat(mapPath).isFile()) { this.logger.warn( - `Guessing the map file name from the source file name: "${basename(mapPath)}"`); + `Guessing the map file name from the source file name: "${PathSegment.basename(mapPath)}"`); try { - externalSourceMap = fromObject(JSON.parse(readFileSync(mapPath, 'utf8'))); + externalSourceMap = fromObject(JSON.parse(this.fs.readFile(mapPath))); } catch (e) { this.logger.error(e); } @@ -333,9 +340,9 @@ export abstract class Renderer { */ protected renderSourceAndMap( sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileInfo[] { - const outputPath = sourceFile.fileName; - const outputMapPath = `${outputPath}.map`; - const relativeSourcePath = basename(outputPath); + const outputPath = AbsoluteFsPath.fromSourceFile(sourceFile); + const outputMapPath = AbsoluteFsPath.fromUnchecked(`${outputPath}.map`); + const relativeSourcePath = PathSegment.basename(outputPath); const relativeMapPath = `${relativeSourcePath}.map`; const outputMap = output.generateMap({ @@ -502,8 +509,8 @@ export function renderDefinitions( return definitions; } -export function stripExtension(filePath: string): string { - return filePath.replace(/\.(js|d\.ts)$/, ''); +export function stripExtension(filePath: T): T { + return filePath.replace(/\.(js|d\.ts)$/, '') as T; } /** diff --git a/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts index 3a5790c85b3b..6bf43e94caa9 100644 --- a/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts @@ -6,15 +6,11 @@ * 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 {dirname} from 'canonical-path'; -import {existsSync, writeFileSync} from 'fs'; -import {mkdir, mv} from 'shelljs'; - +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; import {EntryPoint} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; import {FileInfo} from '../rendering/renderer'; - import {FileWriter} from './file_writer'; /** @@ -22,19 +18,22 @@ import {FileWriter} from './file_writer'; * a back-up of the original file with an extra `.bak` extension. */ export class InPlaceFileWriter implements FileWriter { + constructor(protected fs: FileSystem) {} + writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileInfo[]) { transformedFiles.forEach(file => this.writeFileAndBackup(file)); } + protected writeFileAndBackup(file: FileInfo): void { - mkdir('-p', dirname(file.path)); - const backPath = file.path + '.__ivy_ngcc_bak'; - if (existsSync(backPath)) { + this.fs.ensureDir(AbsoluteFsPath.dirname(file.path)); + const backPath = AbsoluteFsPath.fromUnchecked(`${file.path}.__ivy_ngcc_bak`); + if (this.fs.exists(backPath)) { throw new Error( `Tried to overwrite ${backPath} with an ngcc back up file, which is disallowed.`); } - if (existsSync(file.path)) { - mv(file.path, backPath); + if (this.fs.exists(file.path)) { + this.fs.moveFile(file.path, backPath); } - writeFileSync(file.path, file.contents, 'utf8'); + this.fs.writeFile(file.path, file.contents); } } diff --git a/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts index f95160045f8c..10414ebdfc0c 100644 --- a/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts @@ -6,12 +6,7 @@ * 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 {dirname, join, relative} from 'canonical-path'; -import {writeFileSync} from 'fs'; -import {cp, mkdir} from 'shelljs'; - -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; @@ -32,7 +27,7 @@ const NGCC_DIRECTORY = '__ivy_ngcc__'; export class NewEntryPointFileWriter extends InPlaceFileWriter { writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]) { // The new folder is at the root of the overall package - const ngccFolder = AbsoluteFsPath.fromUnchecked(join(entryPoint.package, NGCC_DIRECTORY)); + const ngccFolder = AbsoluteFsPath.join(entryPoint.package, NGCC_DIRECTORY); this.copyBundle(bundle, entryPoint.package, ngccFolder); transformedFiles.forEach(file => this.writeFile(file, entryPoint.package, ngccFolder)); this.updatePackageJson(entryPoint, bundle.formatProperty, ngccFolder); @@ -41,12 +36,13 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter { protected copyBundle( bundle: EntryPointBundle, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath) { bundle.src.program.getSourceFiles().forEach(sourceFile => { - const relativePath = relative(packagePath, sourceFile.fileName); + const relativePath = + PathSegment.relative(packagePath, AbsoluteFsPath.fromSourceFile(sourceFile)); const isOutsidePackage = relativePath.startsWith('..'); if (!sourceFile.isDeclarationFile && !isOutsidePackage) { - const newFilePath = join(ngccFolder, relativePath); - mkdir('-p', dirname(newFilePath)); - cp(sourceFile.fileName, newFilePath); + const newFilePath = AbsoluteFsPath.join(ngccFolder, relativePath); + this.fs.ensureDir(AbsoluteFsPath.dirname(newFilePath)); + this.fs.copyFile(AbsoluteFsPath.fromSourceFile(sourceFile), newFilePath); } }); } @@ -57,19 +53,24 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter { // This is either `.d.ts` or `.d.ts.map` file super.writeFileAndBackup(file); } else { - const relativePath = relative(packagePath, file.path); - const newFilePath = join(ngccFolder, relativePath); - mkdir('-p', dirname(newFilePath)); - writeFileSync(newFilePath, file.contents, 'utf8'); + const relativePath = PathSegment.relative(packagePath, file.path); + const newFilePath = AbsoluteFsPath.join(ngccFolder, relativePath); + this.fs.ensureDir(AbsoluteFsPath.dirname(newFilePath)); + this.fs.writeFile(newFilePath, file.contents); } } protected updatePackageJson( entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, ngccFolder: AbsoluteFsPath) { - const formatPath = join(entryPoint.path, entryPoint.packageJson[formatProperty] !); - const newFormatPath = join(ngccFolder, relative(entryPoint.package, formatPath)); + const formatPath = + AbsoluteFsPath.join(entryPoint.path, entryPoint.packageJson[formatProperty] !); + const newFormatPath = + AbsoluteFsPath.join(ngccFolder, PathSegment.relative(entryPoint.package, formatPath)); const newFormatProperty = formatProperty + '_ivy_ngcc'; - (entryPoint.packageJson as any)[newFormatProperty] = relative(entryPoint.path, newFormatPath); - writeFileSync(join(entryPoint.path, 'package.json'), JSON.stringify(entryPoint.packageJson)); + (entryPoint.packageJson as any)[newFormatProperty] = + PathSegment.relative(entryPoint.path, newFormatPath); + this.fs.writeFile( + AbsoluteFsPath.join(entryPoint.path, 'package.json'), + JSON.stringify(entryPoint.packageJson)); } } diff --git a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts index c3f629bacda6..afca349809b3 100644 --- a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts @@ -12,6 +12,7 @@ import {Decorator} from '../../../src/ngtsc/reflection'; import {DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform'; import {CompiledClass, DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {MockLogger} from '../helpers/mock_logger'; import {makeTestBundleProgram} from '../helpers/utils'; @@ -136,8 +137,9 @@ describe('DecorationAnalyzer', () => { const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const referencesRegistry = new NgccReferencesRegistry(reflectionHost); + const fs = new NodeJSFileSystem(); const analyzer = new DecorationAnalyzer( - program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, + fs, program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false); testHandler = createTestHandler(); analyzer.handlers = [testHandler]; diff --git a/packages/compiler-cli/ngcc/test/analysis/private_declarations_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/private_declarations_analyzer_spec.ts index 85a7edda8ec6..e4bd4acb3b6a 100644 --- a/packages/compiler-cli/ngcc/test/analysis/private_declarations_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/private_declarations_analyzer_spec.ts @@ -9,12 +9,15 @@ import * as ts from 'typescript'; import {Reference} from '../../../src/ngtsc/imports'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {MockLogger} from '../helpers/mock_logger'; import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils'; +const _ = AbsoluteFsPath.fromUnchecked; + describe('PrivateDeclarationsAnalyzer', () => { describe('analyzeProgram()', () => { @@ -147,11 +150,11 @@ describe('PrivateDeclarationsAnalyzer', () => { // not added to the ReferencesRegistry (i.e. they were not declared in an NgModule). expect(analyses.length).toEqual(2); expect(analyses).toEqual([ - {identifier: 'PrivateComponent1', from: '/src/b.js', dtsFrom: null, alias: null}, + {identifier: 'PrivateComponent1', from: _('/src/b.js'), dtsFrom: null, alias: null}, { identifier: 'InternalComponent1', - from: '/src/c.js', - dtsFrom: '/typings/c.d.ts', + from: _('/src/c.js'), + dtsFrom: _('/typings/c.d.ts'), alias: null }, ]); @@ -207,7 +210,7 @@ describe('PrivateDeclarationsAnalyzer', () => { const analyses = analyzer.analyzeProgram(program); expect(analyses).toEqual([{ identifier: 'ComponentOne', - from: '/src/a.js', + from: _('/src/a.js'), dtsFrom: null, alias: 'aliasedComponentOne', }]); diff --git a/packages/compiler-cli/ngcc/test/analysis/switch_marker_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/switch_marker_analyzer_spec.ts index 5edbfb827b7f..f97ed131e260 100644 --- a/packages/compiler-cli/ngcc/test/analysis/switch_marker_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/switch_marker_analyzer_spec.ts @@ -5,8 +5,6 @@ * 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 ts from 'typescript'; - import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {MockLogger} from '../helpers/mock_logger'; diff --git a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts index 452aef03f5db..b97ee13820b6 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts @@ -9,6 +9,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {EntryPoint} from '../../src/packages/entry_point'; import {MockLogger} from '../helpers/mock_logger'; @@ -18,7 +19,8 @@ describe('DependencyResolver', () => { let host: EsmDependencyHost; let resolver: DependencyResolver; beforeEach(() => { - host = new EsmDependencyHost(new ModuleResolver()); + const fs = new NodeJSFileSystem(); + host = new EsmDependencyHost(fs, new ModuleResolver(fs)); resolver = new DependencyResolver(new MockLogger(), host); }); describe('sortEntryPointsByDependency()', () => { diff --git a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts index 7c1612c739eb..bb3dc79c9b1e 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts @@ -11,12 +11,16 @@ import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; const _ = AbsoluteFsPath.from; describe('DependencyHost', () => { let host: EsmDependencyHost; - beforeEach(() => host = new EsmDependencyHost(new ModuleResolver())); + beforeEach(() => { + const fs = new NodeJSFileSystem(); + host = new EsmDependencyHost(fs, new ModuleResolver(fs)); + }); describe('getDependencies()', () => { beforeEach(createMockFileSystem); @@ -94,13 +98,14 @@ describe('DependencyHost', () => { }); it('should support `paths` alias mappings when resolving modules', () => { - host = new EsmDependencyHost(new ModuleResolver({ - baseUrl: '/dist', - paths: { - '@app/*': ['*'], - '@lib/*/test': ['lib/*/test'], - } - })); + const fs = new NodeJSFileSystem(); + host = new EsmDependencyHost(fs, new ModuleResolver(fs, { + baseUrl: '/dist', + paths: { + '@app/*': ['*'], + '@lib/*/test': ['lib/*/test'], + } + })); const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js')); expect(dependencies.size).toBe(4); expect(dependencies.has(_('/dist/components'))).toBe(true); diff --git a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts index 550c66fcd3ac..b6c96c824d47 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts @@ -10,6 +10,7 @@ import * as mockFs from 'mock-fs'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {ModuleResolver, ResolvedDeepImport, ResolvedExternalModule, ResolvedRelativeModule} from '../../src/dependencies/module_resolver'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; const _ = AbsoluteFsPath.from; @@ -78,7 +79,8 @@ describe('ModuleResolver', () => { describe('resolveModule()', () => { describe('with relative paths', () => { it('should resolve sibling, child and aunt modules', () => { - const resolver = new ModuleResolver(); + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver(fs); expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js'))) .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js'))) @@ -88,14 +90,16 @@ describe('ModuleResolver', () => { }); it('should return `null` if the resolved module relative module does not exist', () => { - const resolver = new ModuleResolver(); + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver(fs); expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null); }); }); describe('with non-mapped external paths', () => { it('should resolve to the package.json of a local node_modules package', () => { - const resolver = new ModuleResolver(); + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver(fs); expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); expect( @@ -106,7 +110,8 @@ describe('ModuleResolver', () => { }); it('should resolve to the package.json of a higher node_modules package', () => { - const resolver = new ModuleResolver(); + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver(fs); expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/libs/node_modules/package-2'))); expect(resolver.resolveModuleImport('top-package', _('/libs/local-package/index.js'))) @@ -114,20 +119,23 @@ describe('ModuleResolver', () => { }); it('should return `null` if the package cannot be found', () => { - const resolver = new ModuleResolver(); + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver(fs); expect(resolver.resolveModuleImport('missing-2', _('/libs/local-package/index.js'))) .toBe(null); }); it('should return `null` if the package is not accessible because it is in a inner node_modules package', () => { - const resolver = new ModuleResolver(); + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver(fs); expect(resolver.resolveModuleImport('package-3', _('/libs/local-package/index.js'))) .toBe(null); }); it('should identify deep imports into an external module', () => { - const resolver = new ModuleResolver(); + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver(fs); expect( resolver.resolveModuleImport('package-1/sub-folder', _('/libs/local-package/index.js'))) .toEqual( @@ -137,8 +145,9 @@ describe('ModuleResolver', () => { describe('with mapped path external modules', () => { it('should resolve to the package.json of simple mapped packages', () => { + const fs = new NodeJSFileSystem(); const resolver = - new ModuleResolver({baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); + new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); @@ -148,7 +157,8 @@ describe('ModuleResolver', () => { }); it('should select the best match by the length of prefix before the *', () => { - const resolver = new ModuleResolver({ + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver(fs, { baseUrl: '/dist', paths: { '@lib/*': ['*'], @@ -166,25 +176,28 @@ describe('ModuleResolver', () => { it('should follow the ordering of `paths` when matching mapped packages', () => { let resolver: ModuleResolver; - resolver = new ModuleResolver({baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); + const fs = new NodeJSFileSystem(); + resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); - resolver = new ModuleResolver({baseUrl: '/dist', paths: {'*': ['sub-folder/*', '*']}}); + resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['sub-folder/*', '*']}}); expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); }); it('should resolve packages when the path mappings have post-fixes', () => { + const fs = new NodeJSFileSystem(); const resolver = - new ModuleResolver({baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}}); + new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}}); expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); }); it('should match paths against complex path matchers', () => { + const fs = new NodeJSFileSystem(); const resolver = - new ModuleResolver({baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}}); + new ModuleResolver(fs, {baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}}); expect(resolver.resolveModuleImport('@shared/package-4', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) @@ -193,15 +206,17 @@ describe('ModuleResolver', () => { it('should resolve path as "relative" if the mapped path is inside the current package', () => { - const resolver = new ModuleResolver({baseUrl: '/dist', paths: {'@shared/*': ['*']}}); + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'@shared/*': ['*']}}); expect(resolver.resolveModuleImport( '@shared/package-4/x', _('/dist/package-4/sub-folder/index.js'))) .toEqual(new ResolvedRelativeModule(_('/dist/package-4/x.js'))); }); it('should resolve paths where the wildcard matches more than one path segment', () => { - const resolver = - new ModuleResolver({baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}}); + const fs = new NodeJSFileSystem(); + const resolver = new ModuleResolver( + fs, {baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}}); expect( resolver.resolveModuleImport( '@shared/sub-folder/package-5/post-fix', _('/dist/package-4/sub-folder/index.js'))) diff --git a/packages/compiler-cli/ngcc/test/helpers/utils.ts b/packages/compiler-cli/ngcc/test/helpers/utils.ts index 6969c51ea52f..0cd665c2465f 100644 --- a/packages/compiler-cli/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/ngcc/test/helpers/utils.ts @@ -15,6 +15,7 @@ import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript'; +const _ = AbsoluteFsPath.fromUnchecked; /** * * @param format The format of the bundle. @@ -28,11 +29,7 @@ export function makeTestEntryPointBundle( const src = makeTestBundleProgram(files); const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null; const isFlatCore = isCore && src.r3SymbolsFile === null; - return { - formatProperty, - format, - rootDirs: [AbsoluteFsPath.fromUnchecked('/')], src, dts, isCore, isFlatCore - }; + return {formatProperty, format, rootDirs: [_('/')], src, dts, isCore, isFlatCore}; } /** @@ -41,10 +38,10 @@ export function makeTestEntryPointBundle( */ export function makeTestBundleProgram(files: {name: string, contents: string}[]): BundleProgram { const {program, options, host} = makeTestProgramInternal(...files); - const path = files[0].name; + const path = _(files[0].name); const file = program.getSourceFile(path) !; const r3SymbolsInfo = files.find(file => file.name.indexOf('r3_symbols') !== -1) || null; - const r3SymbolsPath = r3SymbolsInfo && r3SymbolsInfo.name; + const r3SymbolsPath = r3SymbolsInfo && _(r3SymbolsInfo.name); const r3SymbolsFile = r3SymbolsPath && program.getSourceFile(r3SymbolsPath) || null; return {program, options, host, path, file, r3SymbolsPath, r3SymbolsFile}; } diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts index 1e61fc583c77..98e20267ffae 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ClassMemberKind, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, CtorParameter, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {MockLogger} from '../helpers/mock_logger'; import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils'; @@ -1007,7 +1007,7 @@ describe('Esm2015ReflectionHost', () => { const parameters = host.getConstructorParameters(classNode) !; expect(parameters.length).toBe(1); - expect(parameters[0]).toEqual(jasmine.objectContaining({ + expect(parameters[0]).toEqual(jasmine.objectContaining({ name: 'arg1', decorators: [], })); @@ -1021,7 +1021,7 @@ describe('Esm2015ReflectionHost', () => { const parameters = host.getConstructorParameters(classNode) !; expect(parameters.length).toBe(1); - expect(parameters[0]).toEqual(jasmine.objectContaining({ + expect(parameters[0]).toEqual(jasmine.objectContaining({ name: 'arg1', decorators: null, })); @@ -1035,7 +1035,7 @@ describe('Esm2015ReflectionHost', () => { const parameters = host.getConstructorParameters(classNode) !; expect(parameters.length).toBe(1); - expect(parameters[0]).toEqual(jasmine.objectContaining({ + expect(parameters[0]).toEqual(jasmine.objectContaining({ name: 'arg1', decorators: null, })); @@ -1115,11 +1115,11 @@ describe('Esm2015ReflectionHost', () => { const parameters = host.getConstructorParameters(classNode); expect(parameters !.length).toBe(2); - expect(parameters ![0]).toEqual(jasmine.objectContaining({ + expect(parameters ![0]).toEqual(jasmine.objectContaining({ name: 'arg1', decorators: null, })); - expect(parameters ![1]).toEqual(jasmine.objectContaining({ + expect(parameters ![1]).toEqual(jasmine.objectContaining({ name: 'arg2', decorators: jasmine.any(Array) as any })); diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 1ad2c9f6aa8f..f84dd23684a6 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -12,6 +12,7 @@ import * as mockFs from 'mock-fs'; import {join} from 'path'; import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../../../test/runfile_helpers'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {mainNgcc} from '../../src/main'; import {markAsProcessed} from '../../src/packages/build_marker'; import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point'; @@ -144,8 +145,10 @@ describe('ngcc main()', () => { const basePath = '/node_modules'; const targetPackageJsonPath = _(join(basePath, packagePath, 'package.json')); const targetPackage = loadPackage(packagePath); - markAsProcessed(targetPackage, targetPackageJsonPath, 'typings'); - properties.forEach(property => markAsProcessed(targetPackage, targetPackageJsonPath, property)); + const fs = new NodeJSFileSystem(); + markAsProcessed(fs, targetPackage, targetPackageJsonPath, 'typings'); + properties.forEach( + property => markAsProcessed(fs, targetPackage, targetPackageJsonPath, property)); } diff --git a/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts b/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts index d57061cc2dd6..da3b3d124f64 100644 --- a/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {readFileSync, writeFileSync} from 'fs'; +import {readFileSync} from 'fs'; import * as mockFs from 'mock-fs'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {hasBeenProcessed, markAsProcessed} from '../../src/packages/build_marker'; -import {EntryPoint} from '../../src/packages/entry_point'; function createMockFileSystem() { mockFs({ @@ -106,21 +106,24 @@ describe('Marker files', () => { expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); - markAsProcessed(pkg, COMMON_PACKAGE_PATH, 'fesm2015'); + const fs = new NodeJSFileSystem(); + + markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); pkg = JSON.parse(readFileSync(COMMON_PACKAGE_PATH, 'utf8')); expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); expect(pkg.__processed_by_ivy_ngcc__.esm5).toBeUndefined(); - markAsProcessed(pkg, COMMON_PACKAGE_PATH, 'esm5'); + markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'esm5'); pkg = JSON.parse(readFileSync(COMMON_PACKAGE_PATH, 'utf8')); expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); expect(pkg.__processed_by_ivy_ngcc__.esm5).toEqual('0.0.0-PLACEHOLDER'); }); it('should update the packageJson object in-place', () => { + const fs = new NodeJSFileSystem(); let pkg = JSON.parse(readFileSync(COMMON_PACKAGE_PATH, 'utf8')); expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); - markAsProcessed(pkg, COMMON_PACKAGE_PATH, 'fesm2015'); + markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); }); }); diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts index e16eef9275a1..c2630db9af45 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts @@ -12,6 +12,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DependencyResolver} from '../../src/dependencies/dependency_resolver'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPointFinder} from '../../src/packages/entry_point_finder'; import {MockLogger} from '../helpers/mock_logger'; @@ -22,12 +23,13 @@ describe('findEntryPoints()', () => { let resolver: DependencyResolver; let finder: EntryPointFinder; beforeEach(() => { + const fs = new NodeJSFileSystem(); resolver = - new DependencyResolver(new MockLogger(), new EsmDependencyHost(new ModuleResolver())); + new DependencyResolver(new MockLogger(), new EsmDependencyHost(fs, new ModuleResolver(fs))); spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []}; }); - finder = new EntryPointFinder(new MockLogger(), resolver); + finder = new EntryPointFinder(fs, new MockLogger(), resolver); }); beforeEach(createMockFileSystem); afterEach(restoreRealFileSystem); diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts index a4c028d32838..7505027557ff 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -6,24 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; import {readFileSync} from 'fs'; import * as mockFs from 'mock-fs'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {getEntryPointInfo} from '../../src/packages/entry_point'; import {MockLogger} from '../helpers/mock_logger'; +const _ = AbsoluteFsPath.fromUnchecked; + describe('getEntryPointInfo()', () => { beforeEach(createMockFileSystem); afterEach(restoreRealFileSystem); - const _ = AbsoluteFsPath.from; const SOME_PACKAGE = _('/some_package'); it('should return an object containing absolute paths to the formats of the specified entry-point', () => { - const entryPoint = - getEntryPointInfo(new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point')); + const fs = new NodeJSFileSystem(); + const entryPoint = getEntryPointInfo( + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point')); expect(entryPoint).toEqual({ name: 'some-package/valid_entry_point', package: SOME_PACKAGE, @@ -35,21 +38,24 @@ describe('getEntryPointInfo()', () => { }); it('should return null if there is no package.json at the entry-point path', () => { - const entryPoint = - getEntryPointInfo(new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json')); + const fs = new NodeJSFileSystem(); + const entryPoint = getEntryPointInfo( + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json')); expect(entryPoint).toBe(null); }); it('should return null if there is no typings or types field in the package.json', () => { + const fs = new NodeJSFileSystem(); const entryPoint = - getEntryPointInfo(new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings')); + getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings')); expect(entryPoint).toBe(null); }); it('should return an object with `compiledByAngular` set to false if there is no metadata.json file next to the typing file', () => { - const entryPoint = - getEntryPointInfo(new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata')); + const fs = new NodeJSFileSystem(); + const entryPoint = getEntryPointInfo( + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata')); expect(entryPoint).toEqual({ name: 'some-package/missing_metadata', package: SOME_PACKAGE, @@ -61,8 +67,9 @@ describe('getEntryPointInfo()', () => { }); it('should work if the typings field is named `types', () => { + const fs = new NodeJSFileSystem(); const entryPoint = getEntryPointInfo( - new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings')); + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings')); expect(entryPoint).toEqual({ name: 'some-package/types_rather_than_typings', package: SOME_PACKAGE, @@ -74,8 +81,9 @@ describe('getEntryPointInfo()', () => { }); it('should work with Angular Material style package.json', () => { + const fs = new NodeJSFileSystem(); const entryPoint = - getEntryPointInfo(new MockLogger(), SOME_PACKAGE, _('/some_package/material_style')); + getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style')); expect(entryPoint).toEqual({ name: 'some_package/material_style', package: SOME_PACKAGE, @@ -87,8 +95,9 @@ describe('getEntryPointInfo()', () => { }); it('should return null if the package.json is not valid JSON', () => { - const entryPoint = - getEntryPointInfo(new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols')); + const fs = new NodeJSFileSystem(); + const entryPoint = getEntryPointInfo( + fs, new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols')); expect(entryPoint).toBe(null); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts index c28d25edb0f2..9b6b80879848 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -5,31 +5,33 @@ * 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 {dirname} from 'canonical-path'; import MagicString from 'magic-string'; import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {EsmRenderer} from '../../src/rendering/esm_renderer'; import {makeTestEntryPointBundle} from '../helpers/utils'; import {MockLogger} from '../helpers/mock_logger'; +const _ = AbsoluteFsPath.fromUnchecked; + function setup(file: {name: string, contents: string}) { const logger = new MockLogger(); const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, [file]) !; const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(logger, false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); - const decorationAnalyses = - new DecorationAnalyzer( - bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, - referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) - .analyzeProgram(); + const fs = new NodeJSFileSystem(); + const decorationAnalyses = new DecorationAnalyzer( + fs, bundle.src.program, bundle.src.options, bundle.src.host, + typeChecker, host, referencesRegistry, [_('/')], false) + .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new EsmRenderer(logger, host, false, bundle); + const renderer = new EsmRenderer(fs, logger, host, false, bundle); return { host, program: bundle.src.program, @@ -136,11 +138,11 @@ import * as i1 from '@angular/common';`); it('should insert the given exports at the end of the source file', () => { const {renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [ - {from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA1'}, - {from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA2'}, - {from: '/some/foo/b.js', dtsFrom: '/some/foo/b.d.ts', identifier: 'ComponentB'}, - {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: _(PROGRAM.name), dtsFrom: _(PROGRAM.name), identifier: 'TopLevelComponent'}, ]); expect(output.toString()).toContain(` // Some other content @@ -153,11 +155,11 @@ export {TopLevelComponent};`); it('should not insert alias exports in js output', () => { const {renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [ - {from: '/some/a.js', alias: 'eComponentA1', identifier: 'ComponentA1'}, - {from: '/some/a.js', alias: 'eComponentA2', identifier: 'ComponentA2'}, - {from: '/some/foo/b.js', alias: 'eComponentB', identifier: 'ComponentB'}, - {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ + {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, + {from: _(PROGRAM.name), alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, ]); const outputString = output.toString(); expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts index 8e6e058633e3..8cf050426a22 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts @@ -5,31 +5,33 @@ * 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 {dirname} from 'canonical-path'; import MagicString from 'magic-string'; import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {Esm5Renderer} from '../../src/rendering/esm5_renderer'; import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; import {MockLogger} from '../helpers/mock_logger'; +const _ = AbsoluteFsPath.fromUnchecked; + function setup(file: {name: string, contents: string}) { const logger = new MockLogger(); const bundle = makeTestEntryPointBundle('module', 'esm5', false, [file]); const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm5ReflectionHost(logger, false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); - const decorationAnalyses = - new DecorationAnalyzer( - bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, - referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) - .analyzeProgram(); + const fs = new NodeJSFileSystem(); + const decorationAnalyses = new DecorationAnalyzer( + fs, bundle.src.program, bundle.src.options, bundle.src.host, + typeChecker, host, referencesRegistry, [_('/')], false) + .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new Esm5Renderer(logger, host, false, bundle); + const renderer = new Esm5Renderer(fs, logger, host, false, bundle); return { host, program: bundle.src.program, @@ -173,11 +175,11 @@ import * as i1 from '@angular/common';`); it('should insert the given exports at the end of the source file', () => { const {renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [ - {from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA1'}, - {from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA2'}, - {from: '/some/foo/b.js', dtsFrom: '/some/foo/b.d.ts', identifier: 'ComponentB'}, - {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: _(PROGRAM.name), dtsFrom: _(PROGRAM.name), identifier: 'TopLevelComponent'}, ]); expect(output.toString()).toContain(` export {A, B, C, NoIife, BadIife}; @@ -190,11 +192,11 @@ export {TopLevelComponent};`); it('should not insert alias exports in js output', () => { const {renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [ - {from: '/some/a.js', alias: 'eComponentA1', identifier: 'ComponentA1'}, - {from: '/some/a.js', alias: 'eComponentA2', identifier: 'ComponentA2'}, - {from: '/some/foo/b.js', alias: 'eComponentB', identifier: 'ComponentB'}, - {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ + {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, + {from: _(PROGRAM.name), alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, ]); const outputString = output.toString(); expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index 1e9e3bd3872b..9738585813ab 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -9,6 +9,7 @@ import * as fs from 'fs'; import MagicString from 'magic-string'; import * as ts from 'typescript'; import {fromObject, generateMapFileComment} from 'convert-source-map'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; @@ -20,11 +21,16 @@ import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; import {makeTestEntryPointBundle} from '../helpers/utils'; import {Logger} from '../../src/logging/logger'; import {MockLogger} from '../helpers/mock_logger'; +import {FileSystem} from '../../src/file_system/file_system'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; + +const _ = AbsoluteFsPath.fromUnchecked; class TestRenderer extends Renderer { constructor( - logger: Logger, host: Esm2015ReflectionHost, isCore: boolean, bundle: EntryPointBundle) { - super(logger, host, isCore, bundle); + fs: FileSystem, logger: Logger, host: Esm2015ReflectionHost, isCore: boolean, + bundle: EntryPointBundle) { + super(fs, logger, host, isCore, bundle); } addImports( output: MagicString, imports: {specifier: string, qualifier: string}[], sf: ts.SourceFile) { @@ -59,8 +65,9 @@ function createTestRenderer( const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts); const referencesRegistry = new NgccReferencesRegistry(host); + const fs = new NodeJSFileSystem(); const decorationAnalyses = new DecorationAnalyzer( - bundle.src.program, bundle.src.options, bundle.src.host, + fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); @@ -68,7 +75,7 @@ function createTestRenderer( new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); const privateDeclarationsAnalyses = new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); - const renderer = new TestRenderer(logger, host, isCore, bundle); + const renderer = new TestRenderer(fs, logger, host, isCore, bundle); spyOn(renderer, 'addImports').and.callThrough(); spyOn(renderer, 'addDefinitions').and.callThrough(); spyOn(renderer, 'removeDecorators').and.callThrough(); @@ -354,7 +361,7 @@ describe('Renderer', () => { // Add a mock export to trigger export rendering privateDeclarationsAnalyses.push( - {identifier: 'ComponentB', from: '/src/file.js', dtsFrom: '/typings/b.d.ts'}); + {identifier: 'ComponentB', from: _('/src/file.js'), dtsFrom: _('/typings/b.d.ts')}); const result = renderer.renderProgram( decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, diff --git a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts index 96dcb76d4aa1..c47474af37cd 100644 --- a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts @@ -9,10 +9,14 @@ import {existsSync, readFileSync} from 'fs'; import * as mockFs from 'mock-fs'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; import {InPlaceFileWriter} from '../../src/writing/in_place_file_writer'; +const _ = AbsoluteFsPath.fromUnchecked; + function createMockFileSystem() { mockFs({ '/package/path': { @@ -39,12 +43,13 @@ describe('InPlaceFileWriter', () => { afterEach(restoreRealFileSystem); it('should write all the FileInfo to the disk', () => { - const fileWriter = new InPlaceFileWriter(); + const fs = new NodeJSFileSystem(); + const fileWriter = new InPlaceFileWriter(fs); fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ - {path: '/package/path/top-level.js', contents: 'MODIFIED TOP LEVEL'}, - {path: '/package/path/folder-1/file-1.js', contents: 'MODIFIED FILE 1'}, - {path: '/package/path/folder-2/file-4.js', contents: 'MODIFIED FILE 4'}, - {path: '/package/path/folder-3/file-5.js', contents: 'NEW FILE 5'}, + {path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, + {path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'}, + {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, + {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, ]); expect(readFileSync('/package/path/top-level.js', 'utf8')).toEqual('MODIFIED TOP LEVEL'); expect(readFileSync('/package/path/folder-1/file-1.js', 'utf8')).toEqual('MODIFIED FILE 1'); @@ -55,12 +60,13 @@ describe('InPlaceFileWriter', () => { }); it('should create backups of all files that previously existed', () => { - const fileWriter = new InPlaceFileWriter(); + const fs = new NodeJSFileSystem(); + const fileWriter = new InPlaceFileWriter(fs); fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ - {path: '/package/path/top-level.js', contents: 'MODIFIED TOP LEVEL'}, - {path: '/package/path/folder-1/file-1.js', contents: 'MODIFIED FILE 1'}, - {path: '/package/path/folder-2/file-4.js', contents: 'MODIFIED FILE 4'}, - {path: '/package/path/folder-3/file-5.js', contents: 'NEW FILE 5'}, + {path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, + {path: _('/package/path/folder-1/file-1.js'), contents: 'MODIFIED FILE 1'}, + {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, + {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, ]); expect(readFileSync('/package/path/top-level.js.__ivy_ngcc_bak', 'utf8')) .toEqual('ORIGINAL TOP LEVEL'); @@ -74,12 +80,13 @@ describe('InPlaceFileWriter', () => { }); it('should error if the backup file already exists', () => { - const fileWriter = new InPlaceFileWriter(); + const fs = new NodeJSFileSystem(); + const fileWriter = new InPlaceFileWriter(fs); expect( () => fileWriter.writeBundle( {} as EntryPoint, {} as EntryPointBundle, [ - {path: '/package/path/already-backed-up.js', contents: 'MODIFIED BACKED UP'}, + {path: _('/package/path/already-backed-up.js'), contents: 'MODIFIED BACKED UP'}, ])) .toThrowError( 'Tried to overwrite /package/path/already-backed-up.js.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.'); diff --git a/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts index 60d44a39b253..5775af81ffaf 100644 --- a/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts @@ -10,6 +10,8 @@ import {existsSync, readFileSync} from 'fs'; import * as mockFs from 'mock-fs'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem} from '../../src/file_system/file_system'; +import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo} from '../../src/packages/entry_point'; import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; import {FileWriter} from '../../src/writing/file_writer'; @@ -81,6 +83,7 @@ describe('NewEntryPointFileWriter', () => { beforeEach(createMockFileSystem); afterEach(restoreRealFileSystem); + let fs: FileSystem; let fileWriter: FileWriter; let entryPoint: EntryPoint; let esm5bundle: EntryPointBundle; @@ -88,17 +91,21 @@ describe('NewEntryPointFileWriter', () => { describe('writeBundle() [primary entry-point]', () => { beforeEach(() => { - fileWriter = new NewEntryPointFileWriter(); - entryPoint = - getEntryPointInfo(new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !; - esm5bundle = makeTestBundle(entryPoint, 'module', 'esm5'); - esm2015bundle = makeTestBundle(entryPoint, 'es2015', 'esm2015'); + fs = new NodeJSFileSystem(); + fileWriter = new NewEntryPointFileWriter(fs); + entryPoint = getEntryPointInfo( + fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !; + esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); + esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); }); it('should write the modified files to a new folder', () => { fileWriter.writeBundle(entryPoint, esm5bundle, [ - {path: '/node_modules/test/esm5.js', contents: 'export function FooTop() {} // MODIFIED'}, - {path: '/node_modules/test/esm5.js.map', contents: 'MODIFIED MAPPING DATA'}, + { + path: _('/node_modules/test/esm5.js'), + contents: 'export function FooTop() {} // MODIFIED' + }, + {path: _('/node_modules/test/esm5.js.map'), contents: 'MODIFIED MAPPING DATA'}, ]); expect(readFileSync('/node_modules/test/__ivy_ngcc__/esm5.js', 'utf8')) .toEqual('export function FooTop() {} // MODIFIED'); @@ -112,7 +119,10 @@ describe('NewEntryPointFileWriter', () => { it('should also copy unmodified files in the program', () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ - {path: '/node_modules/test/es2015/foo.js', contents: 'export class FooTop {} // MODIFIED'}, + { + path: _('/node_modules/test/es2015/foo.js'), + contents: 'export class FooTop {} // MODIFIED' + }, ]); expect(readFileSync('/node_modules/test/__ivy_ngcc__/es2015/foo.js', 'utf8')) .toEqual('export class FooTop {} // MODIFIED'); @@ -126,14 +136,20 @@ describe('NewEntryPointFileWriter', () => { it('should update the package.json properties', () => { fileWriter.writeBundle(entryPoint, esm5bundle, [ - {path: '/node_modules/test/esm5.js', contents: 'export function FooTop() {} // MODIFIED'}, + { + path: _('/node_modules/test/esm5.js'), + contents: 'export function FooTop() {} // MODIFIED' + }, ]); expect(loadPackageJson('/node_modules/test')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '__ivy_ngcc__/esm5.js', })); fileWriter.writeBundle(entryPoint, esm2015bundle, [ - {path: '/node_modules/test/es2015/foo.js', contents: 'export class FooTop {} // MODIFIED'}, + { + path: _('/node_modules/test/es2015/foo.js'), + contents: 'export class FooTop {} // MODIFIED' + }, ]); expect(loadPackageJson('/node_modules/test')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '__ivy_ngcc__/esm5.js', @@ -144,10 +160,10 @@ describe('NewEntryPointFileWriter', () => { it('should overwrite and backup typings files', () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ { - path: '/node_modules/test/index.d.ts', - contents: 'export declare class FooTop {} // MODIFIED' + path: _('/node_modules/test/index.d.ts'), + contents: 'export declare class FooTop {} // MODIFIED', }, - {path: '/node_modules/test/index.d.ts.map', contents: 'MODIFIED MAPPING DATA'}, + {path: _('/node_modules/test/index.d.ts.map'), contents: 'MODIFIED MAPPING DATA'}, ]); expect(readFileSync('/node_modules/test/index.d.ts', 'utf8')) .toEqual('export declare class FooTop {} // MODIFIED'); @@ -165,16 +181,20 @@ describe('NewEntryPointFileWriter', () => { describe('writeBundle() [secondary entry-point]', () => { beforeEach(() => { - fileWriter = new NewEntryPointFileWriter(); - entryPoint = - getEntryPointInfo(new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !; - esm5bundle = makeTestBundle(entryPoint, 'module', 'esm5'); - esm2015bundle = makeTestBundle(entryPoint, 'es2015', 'esm2015'); + fs = new NodeJSFileSystem(); + fileWriter = new NewEntryPointFileWriter(fs); + entryPoint = getEntryPointInfo( + fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !; + esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); + esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); }); it('should write the modified file to a new folder', () => { fileWriter.writeBundle(entryPoint, esm5bundle, [ - {path: '/node_modules/test/a/esm5.js', contents: 'export function FooA() {} // MODIFIED'}, + { + path: _('/node_modules/test/a/esm5.js'), + contents: 'export function FooA() {} // MODIFIED' + }, ]); expect(readFileSync('/node_modules/test/__ivy_ngcc__/a/esm5.js', 'utf8')) .toEqual('export function FooA() {} // MODIFIED'); @@ -184,7 +204,10 @@ describe('NewEntryPointFileWriter', () => { it('should also copy unmodified files in the program', () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ - {path: '/node_modules/test/a/es2015/foo.js', contents: 'export class FooA {} // MODIFIED'}, + { + path: _('/node_modules/test/a/es2015/foo.js'), + contents: 'export class FooA {} // MODIFIED' + }, ]); expect(readFileSync('/node_modules/test/__ivy_ngcc__/a/es2015/foo.js', 'utf8')) .toEqual('export class FooA {} // MODIFIED'); @@ -198,14 +221,20 @@ describe('NewEntryPointFileWriter', () => { it('should update the package.json properties', () => { fileWriter.writeBundle(entryPoint, esm5bundle, [ - {path: '/node_modules/test/a/esm5.js', contents: 'export function FooA() {} // MODIFIED'}, + { + path: _('/node_modules/test/a/esm5.js'), + contents: 'export function FooA() {} // MODIFIED' + }, ]); expect(loadPackageJson('/node_modules/test/a')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', })); fileWriter.writeBundle(entryPoint, esm2015bundle, [ - {path: '/node_modules/test/a/es2015/foo.js', contents: 'export class FooA {} // MODIFIED'}, + { + path: _('/node_modules/test/a/es2015/foo.js'), + contents: 'export class FooA {} // MODIFIED' + }, ]); expect(loadPackageJson('/node_modules/test/a')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', @@ -216,7 +245,7 @@ describe('NewEntryPointFileWriter', () => { it('should overwrite and backup typings files', () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ { - path: '/node_modules/test/a/index.d.ts', + path: _('/node_modules/test/a/index.d.ts'), contents: 'export declare class FooA {} // MODIFIED' }, ]); @@ -230,16 +259,20 @@ describe('NewEntryPointFileWriter', () => { describe('writeBundle() [entry-point (with files placed outside entry-point folder)]', () => { beforeEach(() => { - fileWriter = new NewEntryPointFileWriter(); - entryPoint = - getEntryPointInfo(new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !; - esm5bundle = makeTestBundle(entryPoint, 'module', 'esm5'); - esm2015bundle = makeTestBundle(entryPoint, 'es2015', 'esm2015'); + fs = new NodeJSFileSystem(); + fileWriter = new NewEntryPointFileWriter(fs); + entryPoint = getEntryPointInfo( + fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !; + esm5bundle = makeTestBundle(fs, entryPoint, 'module', 'esm5'); + esm2015bundle = makeTestBundle(fs, entryPoint, 'es2015', 'esm2015'); }); it('should write the modified file to a new folder', () => { fileWriter.writeBundle(entryPoint, esm5bundle, [ - {path: '/node_modules/test/lib/esm5.js', contents: 'export function FooB() {} // MODIFIED'}, + { + path: _('/node_modules/test/lib/esm5.js'), + contents: 'export function FooB() {} // MODIFIED' + }, ]); expect(readFileSync('/node_modules/test/__ivy_ngcc__/lib/esm5.js', 'utf8')) .toEqual('export function FooB() {} // MODIFIED'); @@ -250,7 +283,7 @@ describe('NewEntryPointFileWriter', () => { it('should also copy unmodified files in the program', () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ { - path: '/node_modules/test/lib/es2015/foo.js', + path: _('/node_modules/test/lib/es2015/foo.js'), contents: 'export class FooB {} // MODIFIED' }, ]); @@ -278,7 +311,7 @@ describe('NewEntryPointFileWriter', () => { it('should not copy files outside of the package', () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ { - path: '/node_modules/test/lib/es2015/foo.js', + path: _('/node_modules/test/lib/es2015/foo.js'), contents: 'export class FooB {} // MODIFIED' }, ]); @@ -288,7 +321,10 @@ describe('NewEntryPointFileWriter', () => { it('should update the package.json properties', () => { fileWriter.writeBundle(entryPoint, esm5bundle, [ - {path: '/node_modules/test/lib/esm5.js', contents: 'export function FooB() {} // MODIFIED'}, + { + path: _('/node_modules/test/lib/esm5.js'), + contents: 'export function FooB() {} // MODIFIED' + }, ]); expect(loadPackageJson('/node_modules/test/b')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '../__ivy_ngcc__/lib/esm5.js', @@ -296,7 +332,7 @@ describe('NewEntryPointFileWriter', () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ { - path: '/node_modules/test/lib/es2015/foo.js', + path: _('/node_modules/test/lib/es2015/foo.js'), contents: 'export class FooB {} // MODIFIED' }, ]); @@ -309,7 +345,7 @@ describe('NewEntryPointFileWriter', () => { it('should overwrite and backup typings files', () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ { - path: '/node_modules/test/typings/index.d.ts', + path: _('/node_modules/test/typings/index.d.ts'), contents: 'export declare class FooB {} // MODIFIED' }, ]); @@ -323,9 +359,9 @@ describe('NewEntryPointFileWriter', () => { }); function makeTestBundle( - entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, + fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, format: EntryPointFormat): EntryPointBundle { return makeEntryPointBundle( - entryPoint.path, entryPoint.packageJson[formatProperty] !, entryPoint.typings, false, + fs, entryPoint.path, entryPoint.packageJson[formatProperty] !, entryPoint.typings, false, formatProperty, format, true) !; } From 661b6ae18374e69ad020046e60e0512b958d8f5f Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:57 +0100 Subject: [PATCH 11/13] refactor(ivy): ngcc - add MockFileSystem --- .../test/analysis/decoration_analyzer_spec.ts | 16 +- .../dependencies/dependency_resolver_spec.ts | 4 +- .../dependencies/esm_dependency_host_spec.ts | 127 ++++++------- .../test/dependencies/module_resolver_spec.ts | 62 +++---- .../ngcc/test/helpers/mock_file_system.ts | 173 ++++++++++++++++++ .../compiler-cli/ngcc/test/helpers/utils.ts | 9 + .../ngcc/test/packages/build_marker_spec.ts | 29 +-- .../test/packages/entry_point_finder_spec.ts | 13 +- .../ngcc/test/packages/entry_point_spec.ts | 41 ++--- .../test/rendering/esm2015_renderer_spec.ts | 14 +- .../ngcc/test/rendering/esm5_renderer_spec.ts | 33 ++-- .../ngcc/test/rendering/renderer_spec.ts | 30 +-- .../test/writing/in_place_file_writer_spec.ts | 45 ++--- .../new_entry_point_file_writer_spec.ts | 114 +++++------- 14 files changed, 407 insertions(+), 303 deletions(-) create mode 100644 packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts diff --git a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts index afca349809b3..7dddb28c6bc4 100644 --- a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts @@ -12,14 +12,16 @@ import {Decorator} from '../../../src/ngtsc/reflection'; import {DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform'; import {CompiledClass, DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {Folder, MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; -import {makeTestBundleProgram} from '../helpers/utils'; +import {createFileSystemFromProgramFiles, makeTestBundleProgram} from '../helpers/utils'; + +const _ = AbsoluteFsPath.fromUnchecked; const TEST_PROGRAM = [ { - name: 'test.js', + name: _('/test.js'), contents: ` import {Component, Directive, Injectable} from '@angular/core'; @@ -34,7 +36,7 @@ const TEST_PROGRAM = [ `, }, { - name: 'other.js', + name: _('/other.js'), contents: ` import {Component} from '@angular/core'; @@ -46,7 +48,7 @@ const TEST_PROGRAM = [ const INTERNAL_COMPONENT_PROGRAM = [ { - name: 'entrypoint.js', + name: _('/entrypoint.js'), contents: ` import {Component, NgModule} from '@angular/core'; import {ImportedComponent} from './component'; @@ -62,7 +64,7 @@ const INTERNAL_COMPONENT_PROGRAM = [ ` }, { - name: 'component.js', + name: _('/component.js'), contents: ` import {Component} from '@angular/core'; export class ImportedComponent {} @@ -137,7 +139,7 @@ describe('DecorationAnalyzer', () => { const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const referencesRegistry = new NgccReferencesRegistry(reflectionHost); - const fs = new NodeJSFileSystem(); + const fs = new MockFileSystem(createFileSystemFromProgramFiles(...progArgs)); const analyzer = new DecorationAnalyzer( fs, program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false); diff --git a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts index b97ee13820b6..9b5a5c801620 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts @@ -9,8 +9,8 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {EntryPoint} from '../../src/packages/entry_point'; +import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; const _ = AbsoluteFsPath.from; @@ -19,7 +19,7 @@ describe('DependencyResolver', () => { let host: EsmDependencyHost; let resolver: DependencyResolver; beforeEach(() => { - const fs = new NodeJSFileSystem(); + const fs = new MockFileSystem(); host = new EsmDependencyHost(fs, new ModuleResolver(fs)); resolver = new DependencyResolver(new MockLogger(), host); }); diff --git a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts index bb3dc79c9b1e..bea7dc250f08 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts @@ -5,27 +5,23 @@ * 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 * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; +import {MockFileSystem} from '../helpers/mock_file_system'; const _ = AbsoluteFsPath.from; describe('DependencyHost', () => { let host: EsmDependencyHost; beforeEach(() => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); host = new EsmDependencyHost(fs, new ModuleResolver(fs)); }); 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'); @@ -98,7 +94,7 @@ describe('DependencyHost', () => { }); it('should support `paths` alias mappings when resolving modules', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); host = new EsmDependencyHost(fs, new ModuleResolver(fs, { baseUrl: '/dist', paths: { @@ -115,68 +111,65 @@ describe('DependencyHost', () => { expect(missing.size).toBe(0); expect(deepImports.size).toBe(0); }); - - function createMockFileSystem() { - mockFs({ - '/no/imports/or/re-exports/index.js': '// some text but no import-like statements', - '/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', - '/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', - '/external/imports/index.js': `import {X} from 'lib-1';\nimport {Y} from 'lib-1/sub-1';`, - '/external/imports/package.json': '{"esm2015": "./index.js"}', - '/external/imports/index.metadata.json': 'MOCK METADATA', - '/external/re-exports/index.js': `export {X} from 'lib-1';\nexport {Y} from 'lib-1/sub-1';`, - '/external/re-exports/package.json': '{"esm2015": "./index.js"}', - '/external/re-exports/index.metadata.json': 'MOCK METADATA', - '/external/imports-missing/index.js': - `import {X} from 'lib-1';\nimport {Y} from 'missing';`, - '/external/imports-missing/package.json': '{"esm2015": "./index.js"}', - '/external/imports-missing/index.metadata.json': 'MOCK METADATA', - '/external/deep-import/index.js': `import {Y} from 'lib-1/deep/import';`, - '/external/deep-import/package.json': '{"esm2015": "./index.js"}', - '/external/deep-import/index.metadata.json': 'MOCK METADATA', - '/internal/outer/index.js': `import {X} from '../inner';`, - '/internal/outer/package.json': '{"esm2015": "./index.js"}', - '/internal/outer/index.metadata.json': 'MOCK METADATA', - '/internal/inner/index.js': `import {Y} from 'lib-1/sub-1'; export declare class X {}`, - '/internal/circular-a/index.js': - `import {B} from '../circular-b'; import {X} from '../circular-b'; export {Y} from 'lib-1/sub-1';`, - '/internal/circular-b/index.js': - `import {A} from '../circular-a'; import {Y} from '../circular-a'; export {X} from 'lib-1';`, - '/internal/circular-a/package.json': '{"esm2015": "./index.js"}', - '/internal/circular-a/index.metadata.json': 'MOCK METADATA', - '/re-directed/index.js': `import {Z} from 'lib-1/sub-2';`, - '/re-directed/package.json': '{"esm2015": "./index.js"}', - '/re-directed/index.metadata.json': 'MOCK METADATA', - '/path-alias/index.js': - `import {TestHelper} from '@app/components';\nimport {Service} from '@app/shared';\nimport {TestHelper} from '@lib/shared/test';\nimport {X} from 'lib-1';`, - '/path-alias/package.json': '{"esm2015": "./index.js"}', - '/path-alias/index.metadata.json': 'MOCK METADATA', - '/node_modules/lib-1/index.js': 'export declare class X {}', - '/node_modules/lib-1/package.json': '{"esm2015": "./index.js"}', - '/node_modules/lib-1/index.metadata.json': 'MOCK METADATA', - '/node_modules/lib-1/deep/import/index.js': 'export declare class DeepImport {}', - '/node_modules/lib-1/sub-1/index.js': 'export declare class Y {}', - '/node_modules/lib-1/sub-1/package.json': '{"esm2015": "./index.js"}', - '/node_modules/lib-1/sub-1/index.metadata.json': 'MOCK METADATA', - '/node_modules/lib-1/sub-2.js': `export * from './sub-2/sub-2';`, - '/node_modules/lib-1/sub-2/sub-2.js': `export declare class Z {}';`, - '/node_modules/lib-1/sub-2/package.json': '{"esm2015": "./sub-2.js"}', - '/node_modules/lib-1/sub-2/sub-2.metadata.json': 'MOCK METADATA', - '/dist/components/index.js': `class MyComponent {};`, - '/dist/components/package.json': '{"esm2015": "./index.js"}', - '/dist/components/index.metadata.json': 'MOCK METADATA', - '/dist/shared/index.js': `import {X} from 'lib-1';\nexport class Service {}`, - '/dist/shared/package.json': '{"esm2015": "./index.js"}', - '/dist/shared/index.metadata.json': 'MOCK METADATA', - '/dist/lib/shared/test/index.js': `export class TestHelper {}`, - '/dist/lib/shared/test/package.json': '{"esm2015": "./index.js"}', - '/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA', - }); - } - - function restoreRealFileSystem() { mockFs.restore(); } }); + function createMockFileSystem() { + return new MockFileSystem({ + '/no/imports/or/re-exports/index.js': '// some text but no import-like statements', + '/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', + '/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports/index.js': `import {X} from 'lib-1';\nimport {Y} from 'lib-1/sub-1';`, + '/external/imports/package.json': '{"esm2015": "./index.js"}', + '/external/imports/index.metadata.json': 'MOCK METADATA', + '/external/re-exports/index.js': `export {X} from 'lib-1';\nexport {Y} from 'lib-1/sub-1';`, + '/external/re-exports/package.json': '{"esm2015": "./index.js"}', + '/external/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports-missing/index.js': `import {X} from 'lib-1';\nimport {Y} from 'missing';`, + '/external/imports-missing/package.json': '{"esm2015": "./index.js"}', + '/external/imports-missing/index.metadata.json': 'MOCK METADATA', + '/external/deep-import/index.js': `import {Y} from 'lib-1/deep/import';`, + '/external/deep-import/package.json': '{"esm2015": "./index.js"}', + '/external/deep-import/index.metadata.json': 'MOCK METADATA', + '/internal/outer/index.js': `import {X} from '../inner';`, + '/internal/outer/package.json': '{"esm2015": "./index.js"}', + '/internal/outer/index.metadata.json': 'MOCK METADATA', + '/internal/inner/index.js': `import {Y} from 'lib-1/sub-1'; export declare class X {}`, + '/internal/circular-a/index.js': + `import {B} from '../circular-b'; import {X} from '../circular-b'; export {Y} from 'lib-1/sub-1';`, + '/internal/circular-b/index.js': + `import {A} from '../circular-a'; import {Y} from '../circular-a'; export {X} from 'lib-1';`, + '/internal/circular-a/package.json': '{"esm2015": "./index.js"}', + '/internal/circular-a/index.metadata.json': 'MOCK METADATA', + '/re-directed/index.js': `import {Z} from 'lib-1/sub-2';`, + '/re-directed/package.json': '{"esm2015": "./index.js"}', + '/re-directed/index.metadata.json': 'MOCK METADATA', + '/path-alias/index.js': + `import {TestHelper} from '@app/components';\nimport {Service} from '@app/shared';\nimport {TestHelper} from '@lib/shared/test';\nimport {X} from 'lib-1';`, + '/path-alias/package.json': '{"esm2015": "./index.js"}', + '/path-alias/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib-1/index.js': 'export declare class X {}', + '/node_modules/lib-1/package.json': '{"esm2015": "./index.js"}', + '/node_modules/lib-1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib-1/deep/import/index.js': 'export declare class DeepImport {}', + '/node_modules/lib-1/sub-1/index.js': 'export declare class Y {}', + '/node_modules/lib-1/sub-1/package.json': '{"esm2015": "./index.js"}', + '/node_modules/lib-1/sub-1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib-1/sub-2.js': `export * from './sub-2/sub-2';`, + '/node_modules/lib-1/sub-2/sub-2.js': `export declare class Z {}';`, + '/node_modules/lib-1/sub-2/package.json': '{"esm2015": "./sub-2.js"}', + '/node_modules/lib-1/sub-2/sub-2.metadata.json': 'MOCK METADATA', + '/dist/components/index.js': `class MyComponent {};`, + '/dist/components/package.json': '{"esm2015": "./index.js"}', + '/dist/components/index.metadata.json': 'MOCK METADATA', + '/dist/shared/index.js': `import {X} from 'lib-1';\nexport class Service {}`, + '/dist/shared/package.json': '{"esm2015": "./index.js"}', + '/dist/shared/index.metadata.json': 'MOCK METADATA', + '/dist/lib/shared/test/index.js': `export class TestHelper {}`, + '/dist/lib/shared/test/package.json': '{"esm2015": "./index.js"}', + '/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA', + }); + } + describe('isStringImportOrReexport', () => { it('should return true if the statement is an import', () => { expect(host.isStringImportOrReexport(createStatement('import {X} from "some/x";'))) diff --git a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts index b6c96c824d47..245a191f0ab8 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts @@ -5,17 +5,14 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {ModuleResolver, ResolvedDeepImport, ResolvedExternalModule, ResolvedRelativeModule} from '../../src/dependencies/module_resolver'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; +import {MockFileSystem} from '../helpers/mock_file_system'; const _ = AbsoluteFsPath.from; function createMockFileSystem() { - mockFs({ + return new MockFileSystem({ '/libs': { 'local-package': { 'package.json': 'PACKAGE.JSON for local-package', @@ -68,19 +65,12 @@ function createMockFileSystem() { }); } -function restoreRealFileSystem() { - mockFs.restore(); -} describe('ModuleResolver', () => { - beforeEach(createMockFileSystem); - afterEach(restoreRealFileSystem); - describe('resolveModule()', () => { describe('with relative paths', () => { it('should resolve sibling, child and aunt modules', () => { - const fs = new NodeJSFileSystem(); - const resolver = new ModuleResolver(fs); + const resolver = new ModuleResolver(createMockFileSystem()); expect(resolver.resolveModuleImport('./x', _('/libs/local-package/index.js'))) .toEqual(new ResolvedRelativeModule(_('/libs/local-package/x.js'))); expect(resolver.resolveModuleImport('./sub-folder', _('/libs/local-package/index.js'))) @@ -90,16 +80,14 @@ describe('ModuleResolver', () => { }); it('should return `null` if the resolved module relative module does not exist', () => { - const fs = new NodeJSFileSystem(); - const resolver = new ModuleResolver(fs); + const resolver = new ModuleResolver(createMockFileSystem()); expect(resolver.resolveModuleImport('./y', _('/libs/local-package/index.js'))).toBe(null); }); }); describe('with non-mapped external paths', () => { it('should resolve to the package.json of a local node_modules package', () => { - const fs = new NodeJSFileSystem(); - const resolver = new ModuleResolver(fs); + const resolver = new ModuleResolver(createMockFileSystem()); expect(resolver.resolveModuleImport('package-1', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/libs/local-package/node_modules/package-1'))); expect( @@ -110,8 +98,7 @@ describe('ModuleResolver', () => { }); it('should resolve to the package.json of a higher node_modules package', () => { - const fs = new NodeJSFileSystem(); - const resolver = new ModuleResolver(fs); + const resolver = new ModuleResolver(createMockFileSystem()); expect(resolver.resolveModuleImport('package-2', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/libs/node_modules/package-2'))); expect(resolver.resolveModuleImport('top-package', _('/libs/local-package/index.js'))) @@ -119,23 +106,20 @@ describe('ModuleResolver', () => { }); it('should return `null` if the package cannot be found', () => { - const fs = new NodeJSFileSystem(); - const resolver = new ModuleResolver(fs); + const resolver = new ModuleResolver(createMockFileSystem()); expect(resolver.resolveModuleImport('missing-2', _('/libs/local-package/index.js'))) .toBe(null); }); it('should return `null` if the package is not accessible because it is in a inner node_modules package', () => { - const fs = new NodeJSFileSystem(); - const resolver = new ModuleResolver(fs); + const resolver = new ModuleResolver(createMockFileSystem()); expect(resolver.resolveModuleImport('package-3', _('/libs/local-package/index.js'))) .toBe(null); }); it('should identify deep imports into an external module', () => { - const fs = new NodeJSFileSystem(); - const resolver = new ModuleResolver(fs); + const resolver = new ModuleResolver(createMockFileSystem()); expect( resolver.resolveModuleImport('package-1/sub-folder', _('/libs/local-package/index.js'))) .toEqual( @@ -145,9 +129,8 @@ describe('ModuleResolver', () => { describe('with mapped path external modules', () => { it('should resolve to the package.json of simple mapped packages', () => { - const fs = new NodeJSFileSystem(); - const resolver = - new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); + const resolver = new ModuleResolver( + createMockFileSystem(), {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); @@ -157,8 +140,7 @@ describe('ModuleResolver', () => { }); it('should select the best match by the length of prefix before the *', () => { - const fs = new NodeJSFileSystem(); - const resolver = new ModuleResolver(fs, { + const resolver = new ModuleResolver(createMockFileSystem(), { baseUrl: '/dist', paths: { '@lib/*': ['*'], @@ -176,7 +158,7 @@ describe('ModuleResolver', () => { it('should follow the ordering of `paths` when matching mapped packages', () => { let resolver: ModuleResolver; - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['*', 'sub-folder/*']}}); expect(resolver.resolveModuleImport('package-4', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/package-4'))); @@ -187,17 +169,15 @@ describe('ModuleResolver', () => { }); it('should resolve packages when the path mappings have post-fixes', () => { - const fs = new NodeJSFileSystem(); - const resolver = - new ModuleResolver(fs, {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}}); + const resolver = new ModuleResolver( + createMockFileSystem(), {baseUrl: '/dist', paths: {'*': ['sub-folder/*/post-fix']}}); expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-5/post-fix'))); }); it('should match paths against complex path matchers', () => { - const fs = new NodeJSFileSystem(); - const resolver = - new ModuleResolver(fs, {baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}}); + const resolver = new ModuleResolver( + createMockFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['sub-folder/*']}}); expect(resolver.resolveModuleImport('@shared/package-4', _('/libs/local-package/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/sub-folder/package-4'))); expect(resolver.resolveModuleImport('package-5', _('/libs/local-package/index.js'))) @@ -206,17 +186,17 @@ describe('ModuleResolver', () => { it('should resolve path as "relative" if the mapped path is inside the current package', () => { - const fs = new NodeJSFileSystem(); - const resolver = new ModuleResolver(fs, {baseUrl: '/dist', paths: {'@shared/*': ['*']}}); + const resolver = new ModuleResolver( + createMockFileSystem(), {baseUrl: '/dist', paths: {'@shared/*': ['*']}}); expect(resolver.resolveModuleImport( '@shared/package-4/x', _('/dist/package-4/sub-folder/index.js'))) .toEqual(new ResolvedRelativeModule(_('/dist/package-4/x.js'))); }); it('should resolve paths where the wildcard matches more than one path segment', () => { - const fs = new NodeJSFileSystem(); const resolver = new ModuleResolver( - fs, {baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}}); + createMockFileSystem(), + {baseUrl: '/dist', paths: {'@shared/*/post-fix': ['*/post-fix']}}); expect( resolver.resolveModuleImport( '@shared/sub-folder/package-5/post-fix', _('/dist/package-4/sub-folder/index.js'))) diff --git a/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts b/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts new file mode 100644 index 000000000000..c47e2f359edb --- /dev/null +++ b/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts @@ -0,0 +1,173 @@ +/** + * @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 {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {FileStats, FileSystem} from '../../src/file_system/file_system'; + +/** + * An in-memory file system that can be used in unit tests. + */ +export class MockFileSystem implements FileSystem { + files: Folder = {}; + constructor(...folders: Folder[]) { + folders.forEach(files => this.processFiles(this.files, files)); + } + + exists(path: AbsoluteFsPath): boolean { return this.findFromPath(path) !== null; } + + readFile(path: AbsoluteFsPath): string { + const file = this.findFromPath(path); + if (isFile(file)) { + return file; + } else { + throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); + } + } + + writeFile(path: AbsoluteFsPath, data: string): void { + const [folderPath, basename] = this.splitIntoFolderAndFile(path); + const folder = this.findFromPath(folderPath); + if (!isFolder(folder)) { + throw new MockFileSystemError( + 'ENOENT', path, `Unable to write file "${path}". The containing folder does not exist.`); + } + folder[basename] = data; + } + + readdir(path: AbsoluteFsPath): PathSegment[] { + const folder = this.findFromPath(path); + if (folder === null) { + throw new MockFileSystemError( + 'ENOENT', path, `Unable to read directory "${path}". It does not exist.`); + } + if (isFile(folder)) { + throw new MockFileSystemError( + 'ENOTDIR', path, `Unable to read directory "${path}". It is a file.`); + } + return Object.keys(folder) as PathSegment[]; + } + + lstat(path: AbsoluteFsPath): FileStats { + const fileOrFolder = this.findFromPath(path); + if (fileOrFolder === null) { + throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); + } + return new MockFileStats(fileOrFolder); + } + + stat(path: AbsoluteFsPath): FileStats { + const fileOrFolder = this.findFromPath(path, {followSymLinks: true}); + if (fileOrFolder === null) { + throw new MockFileSystemError('ENOENT', path, `File "${path}" does not exist.`); + } + return new MockFileStats(fileOrFolder); + } + + pwd(): AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked('/'); } + + copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { + this.writeFile(to, this.readFile(from)); + } + + moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { + this.writeFile(to, this.readFile(from)); + const folder = this.findFromPath(AbsoluteFsPath.dirname(from)) as Folder; + const basename = PathSegment.basename(from); + delete folder[basename]; + } + + ensureDir(path: AbsoluteFsPath): void { this.ensureFolders(this.files, path.split('/')); } + + private processFiles(current: Folder, files: Folder): void { + Object.keys(files).forEach(path => { + const segments = path.split('/'); + const lastSegment = segments.pop() !; + const containingFolder = this.ensureFolders(current, segments); + const entity = files[path]; + if (isFolder(entity)) { + const processedFolder = containingFolder[lastSegment] = {} as Folder; + this.processFiles(processedFolder, entity); + } else { + containingFolder[lastSegment] = entity; + } + }); + } + + private ensureFolders(current: Folder, segments: string[]): Folder { + for (const segment of segments) { + if (isFile(current[segment])) { + throw new Error(`Folder already exists as a file.`); + } + if (!current[segment]) { + current[segment] = {}; + } + current = current[segment] as Folder; + } + return current; + } + + private findFromPath(path: AbsoluteFsPath, options?: {followSymLinks: boolean}): Entity|null { + const followSymLinks = !!options && options.followSymLinks; + const segments = path.split('/'); + let current = this.files; + while (segments.length) { + const next: Entity = current[segments.shift() !]; + if (next === undefined) { + return null; + } + if (segments.length > 0 && (!isFolder(next))) { + return null; + } + if (isFile(next)) { + return next; + } + if (isSymLink(next)) { + return followSymLinks ? + this.findFromPath(AbsoluteFsPath.resolve(next.path, ...segments), {followSymLinks}) : + next; + } + current = next; + } + return current || null; + } + + private splitIntoFolderAndFile(path: AbsoluteFsPath): [AbsoluteFsPath, string] { + const segments = path.split('/'); + const file = segments.pop() !; + return [AbsoluteFsPath.fromUnchecked(segments.join('/')), file]; + } +} + +export type Entity = Folder | File | SymLink; +export interface Folder { [pathSegments: string]: Entity; } +export type File = string; +export class SymLink { + constructor(public path: AbsoluteFsPath) {} +} + +class MockFileStats implements FileStats { + constructor(private entity: Entity) {} + isFile(): boolean { return isFile(this.entity); } + isDirectory(): boolean { return isFolder(this.entity); } + isSymbolicLink(): boolean { return isSymLink(this.entity); } +} + +class MockFileSystemError extends Error { + constructor(public code: string, public path: string, message: string) { super(message); } +} + +function isFile(item: Entity | null): item is File { + return typeof item === 'string'; +} + +function isSymLink(item: Entity | null): item is SymLink { + return item instanceof SymLink; +} + +function isFolder(item: Entity | null): item is Folder { + return item !== null && !isFile(item) && !isSymLink(item); +} diff --git a/packages/compiler-cli/ngcc/test/helpers/utils.ts b/packages/compiler-cli/ngcc/test/helpers/utils.ts index 0cd665c2465f..f4eae77e54e0 100644 --- a/packages/compiler-cli/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/ngcc/test/helpers/utils.ts @@ -12,6 +12,7 @@ import {makeProgram} from '../../../src/ngtsc/testing/in_memory_typescript'; import {BundleProgram} from '../../src/packages/bundle_program'; import {EntryPointFormat, EntryPointJsonProperty} from '../../src/packages/entry_point'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; +import {Folder} from './mock_file_system'; export {getDeclaration} from '../../../src/ngtsc/testing/in_memory_typescript'; @@ -121,3 +122,11 @@ export function convertToDirectTsLibImport(filesystem: {name: string, contents: return {...file, contents}; }); } + +export function createFileSystemFromProgramFiles( + ...fileCollections: ({name: string, contents: string}[] | undefined)[]): Folder { + const folder: Folder = {}; + fileCollections.forEach( + files => files && files.forEach(file => folder[file.name] = file.contents)); + return folder; +} diff --git a/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts b/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts index da3b3d124f64..8e12ce8d37d2 100644 --- a/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts @@ -5,16 +5,12 @@ * 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 {readFileSync} from 'fs'; -import * as mockFs from 'mock-fs'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {hasBeenProcessed, markAsProcessed} from '../../src/packages/build_marker'; +import {MockFileSystem} from '../helpers/mock_file_system'; function createMockFileSystem() { - mockFs({ + return new MockFileSystem({ '/node_modules/@angular/common': { 'package.json': `{ "fesm2015": "./fesm2015/common.js", @@ -90,38 +86,31 @@ function createMockFileSystem() { }); } -function restoreRealFileSystem() { - mockFs.restore(); -} - describe('Marker files', () => { - beforeEach(createMockFileSystem); - afterEach(restoreRealFileSystem); - const COMMON_PACKAGE_PATH = AbsoluteFsPath.from('/node_modules/@angular/common/package.json'); describe('markAsProcessed', () => { it('should write a property in the package.json containing the version placeholder', () => { - let pkg = JSON.parse(readFileSync(COMMON_PACKAGE_PATH, 'utf8')); + const fs = createMockFileSystem(); + + let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); - const fs = new NodeJSFileSystem(); - markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); - pkg = JSON.parse(readFileSync(COMMON_PACKAGE_PATH, 'utf8')); + pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); expect(pkg.__processed_by_ivy_ngcc__.esm5).toBeUndefined(); markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'esm5'); - pkg = JSON.parse(readFileSync(COMMON_PACKAGE_PATH, 'utf8')); + pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); expect(pkg.__processed_by_ivy_ngcc__.esm5).toEqual('0.0.0-PLACEHOLDER'); }); it('should update the packageJson object in-place', () => { - const fs = new NodeJSFileSystem(); - let pkg = JSON.parse(readFileSync(COMMON_PACKAGE_PATH, 'utf8')); + const fs = createMockFileSystem(); + let pkg = JSON.parse(fs.readFile(COMMON_PACKAGE_PATH)); expect(pkg.__processed_by_ivy_ngcc__).toBeUndefined(); markAsProcessed(fs, pkg, COMMON_PACKAGE_PATH, 'fesm2015'); expect(pkg.__processed_by_ivy_ngcc__.fesm2015).toEqual('0.0.0-PLACEHOLDER'); diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts index c2630db9af45..e93af3918566 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts @@ -6,15 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import * as mockFs from 'mock-fs'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DependencyResolver} from '../../src/dependencies/dependency_resolver'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPointFinder} from '../../src/packages/entry_point_finder'; +import {MockFileSystem, SymLink} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; const _ = AbsoluteFsPath.from; @@ -23,7 +21,7 @@ describe('findEntryPoints()', () => { let resolver: DependencyResolver; let finder: EntryPointFinder; beforeEach(() => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); resolver = new DependencyResolver(new MockLogger(), new EsmDependencyHost(fs, new ModuleResolver(fs))); spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { @@ -31,8 +29,6 @@ describe('findEntryPoints()', () => { }); finder = new EntryPointFinder(fs, new MockLogger(), resolver); }); - beforeEach(createMockFileSystem); - afterEach(restoreRealFileSystem); it('should find sub-entry-points within a package', () => { const {entryPoints} = finder.findEntryPoints(_('/sub_entry_points')); @@ -90,7 +86,7 @@ describe('findEntryPoints()', () => { }); function createMockFileSystem() { - mockFs({ + return new MockFileSystem({ '/sub_entry_points': { 'common': { 'package.json': createPackageJson('common'), @@ -142,7 +138,7 @@ describe('findEntryPoints()', () => { }, }, '/symlinked_folders': { - 'common': mockFs.symlink({path: '/sub_entry_points/common'}), + 'common': new SymLink(_('/sub_entry_points/common')), }, '/nested_node_modules': { 'outer': { @@ -158,7 +154,6 @@ describe('findEntryPoints()', () => { }, }); } - function restoreRealFileSystem() { mockFs.restore(); } }); function createPackageJson(packageName: string): string { diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts index 7505027557ff..375fa013ac81 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -6,25 +6,20 @@ * found in the LICENSE file at https://angular.io/license */ -import {readFileSync} from 'fs'; -import * as mockFs from 'mock-fs'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; +import {FileSystem} from '../../src/file_system/file_system'; import {getEntryPointInfo} from '../../src/packages/entry_point'; +import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; const _ = AbsoluteFsPath.fromUnchecked; describe('getEntryPointInfo()', () => { - beforeEach(createMockFileSystem); - afterEach(restoreRealFileSystem); - const SOME_PACKAGE = _('/some_package'); it('should return an object containing absolute paths to the formats of the specified entry-point', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo( fs, new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point')); expect(entryPoint).toEqual({ @@ -32,20 +27,20 @@ describe('getEntryPointInfo()', () => { package: SOME_PACKAGE, path: _('/some_package/valid_entry_point'), typings: _(`/some_package/valid_entry_point/valid_entry_point.d.ts`), - packageJson: loadPackageJson('/some_package/valid_entry_point'), + packageJson: loadPackageJson(fs, '/some_package/valid_entry_point'), compiledByAngular: true, }); }); it('should return null if there is no package.json at the entry-point path', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo( fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json')); expect(entryPoint).toBe(null); }); it('should return null if there is no typings or types field in the package.json', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings')); expect(entryPoint).toBe(null); @@ -53,7 +48,7 @@ describe('getEntryPointInfo()', () => { it('should return an object with `compiledByAngular` set to false if there is no metadata.json file next to the typing file', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo( fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata')); expect(entryPoint).toEqual({ @@ -61,13 +56,13 @@ describe('getEntryPointInfo()', () => { package: SOME_PACKAGE, path: _('/some_package/missing_metadata'), typings: _(`/some_package/missing_metadata/missing_metadata.d.ts`), - packageJson: loadPackageJson('/some_package/missing_metadata'), + packageJson: loadPackageJson(fs, '/some_package/missing_metadata'), compiledByAngular: false, }); }); it('should work if the typings field is named `types', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo( fs, new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings')); expect(entryPoint).toEqual({ @@ -75,13 +70,13 @@ describe('getEntryPointInfo()', () => { package: SOME_PACKAGE, path: _('/some_package/types_rather_than_typings'), typings: _(`/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`), - packageJson: loadPackageJson('/some_package/types_rather_than_typings'), + packageJson: loadPackageJson(fs, '/some_package/types_rather_than_typings'), compiledByAngular: true, }); }); it('should work with Angular Material style package.json', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style')); expect(entryPoint).toEqual({ @@ -89,13 +84,13 @@ describe('getEntryPointInfo()', () => { package: SOME_PACKAGE, path: _('/some_package/material_style'), typings: _(`/some_package/material_style/material_style.d.ts`), - packageJson: JSON.parse(readFileSync('/some_package/material_style/package.json', 'utf8')), + packageJson: loadPackageJson(fs, '/some_package/material_style'), compiledByAngular: true, }); }); it('should return null if the package.json is not valid JSON', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const entryPoint = getEntryPointInfo( fs, new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols')); expect(entryPoint).toBe(null); @@ -103,7 +98,7 @@ describe('getEntryPointInfo()', () => { }); function createMockFileSystem() { - mockFs({ + return new MockFileSystem({ '/some_package': { 'valid_entry_point': { 'package.json': createPackageJson('valid_entry_point'), @@ -150,10 +145,6 @@ function createMockFileSystem() { }); } -function restoreRealFileSystem() { - mockFs.restore(); -} - function createPackageJson( packageName: string, {excludes}: {excludes?: string[]} = {}, typingsProp: string = 'typings'): string { @@ -172,6 +163,6 @@ function createPackageJson( return JSON.stringify(packageJson); } -export function loadPackageJson(packagePath: string) { - return JSON.parse(readFileSync(packagePath + '/package.json', 'utf8')); +export function loadPackageJson(fs: FileSystem, packagePath: string) { + return JSON.parse(fs.readFile(_(packagePath + '/package.json'))); } diff --git a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts index 9b6b80879848..34ba2583bfe5 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -11,21 +11,21 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {EsmRenderer} from '../../src/rendering/esm_renderer'; import {makeTestEntryPointBundle} from '../helpers/utils'; +import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; const _ = AbsoluteFsPath.fromUnchecked; -function setup(file: {name: string, contents: string}) { +function setup(file: {name: AbsoluteFsPath, contents: string}) { + const fs = new MockFileSystem(); const logger = new MockLogger(); const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, [file]) !; const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(logger, false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); - const fs = new NodeJSFileSystem(); const decorationAnalyses = new DecorationAnalyzer( fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, referencesRegistry, [_('/')], false) @@ -40,7 +40,7 @@ function setup(file: {name: string, contents: string}) { } const PROGRAM = { - name: '/some/file.js', + name: _('/some/file.js'), contents: ` /* A copyright notice */ import 'some-side-effect'; @@ -76,7 +76,7 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { }; const PROGRAM_DECORATE_HELPER = { - name: '/some/file.js', + name: _('/some/file.js'), contents: ` import * as tslib_1 from "tslib"; var D_1; @@ -142,7 +142,7 @@ import * as i1 from '@angular/common';`); {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, - {from: _(PROGRAM.name), dtsFrom: _(PROGRAM.name), identifier: 'TopLevelComponent'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, ]); expect(output.toString()).toContain(` // Some other content @@ -159,7 +159,7 @@ export {TopLevelComponent};`); {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, - {from: _(PROGRAM.name), alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, ]); const outputString = output.toString(); expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts index 8cf050426a22..83c8b253cf0c 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts @@ -11,25 +11,26 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {Esm5Renderer} from '../../src/rendering/esm5_renderer'; import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; +import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; const _ = AbsoluteFsPath.fromUnchecked; -function setup(file: {name: string, contents: string}) { +function setup(file: {name: AbsoluteFsPath, contents: string}) { + const fs = new MockFileSystem(); const logger = new MockLogger(); const bundle = makeTestEntryPointBundle('module', 'esm5', false, [file]); const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm5ReflectionHost(logger, false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); - const fs = new NodeJSFileSystem(); - const decorationAnalyses = new DecorationAnalyzer( - fs, bundle.src.program, bundle.src.options, bundle.src.host, - typeChecker, host, referencesRegistry, [_('/')], false) - .analyzeProgram(); + const decorationAnalyses = + new DecorationAnalyzer( + fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, + referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) + .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new Esm5Renderer(fs, logger, host, false, bundle); return { @@ -40,7 +41,7 @@ function setup(file: {name: string, contents: string}) { } const PROGRAM = { - name: '/some/file.js', + name: _('/some/file.js'), contents: ` /* A copyright notice */ import 'some-side-effect'; @@ -100,7 +101,7 @@ export {A, B, C, NoIife, BadIife};` }; const PROGRAM_DECORATE_HELPER = { - name: '/some/file.js', + name: _('/some/file.js'), contents: ` import * as tslib_1 from "tslib"; /* A copyright notice */ @@ -179,7 +180,7 @@ import * as i1 from '@angular/common';`); {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, - {from: _(PROGRAM.name), dtsFrom: _(PROGRAM.name), identifier: 'TopLevelComponent'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, ]); expect(output.toString()).toContain(` export {A, B, C, NoIife, BadIife}; @@ -193,10 +194,10 @@ export {TopLevelComponent};`); const {renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, - {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, - {from: _(PROGRAM.name), alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + {from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, ]); const outputString = output.toString(); expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); @@ -285,14 +286,14 @@ SOME DEFINITION TEXT const noIifeDeclaration = getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); - const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'}; + const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: _('NoIife')}; expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) .toThrowError( 'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); const badIifeDeclaration = getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); - const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'}; + const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: _('BadIife')}; expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) .toThrowError( 'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js'); diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index 9738585813ab..f47ac9b248bc 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -5,7 +5,6 @@ * 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 fs from 'fs'; import MagicString from 'magic-string'; import * as ts from 'typescript'; import {fromObject, generateMapFileComment} from 'convert-source-map'; @@ -18,11 +17,11 @@ import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {RedundantDecoratorMap, Renderer} from '../../src/rendering/renderer'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; -import {makeTestEntryPointBundle} from '../helpers/utils'; +import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; import {Logger} from '../../src/logging/logger'; +import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; import {FileSystem} from '../../src/file_system/file_system'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; const _ = AbsoluteFsPath.fromUnchecked; @@ -58,14 +57,15 @@ class TestRenderer extends Renderer { function createTestRenderer( packageName: string, files: {name: string, contents: string}[], - dtsFiles?: {name: string, contents: string}[]) { + dtsFiles?: {name: string, contents: string}[], + mappingFiles?: {name: string, contents: string}[]) { const logger = new MockLogger(); + const fs = new MockFileSystem(createFileSystemFromProgramFiles(files, dtsFiles, mappingFiles)); const isCore = packageName === '@angular/core'; const bundle = makeTestEntryPointBundle('es2015', 'esm2015', isCore, files, dtsFiles); const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts); const referencesRegistry = new NgccReferencesRegistry(host); - const fs = new NodeJSFileSystem(); const decorationAnalyses = new DecorationAnalyzer( fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) @@ -200,8 +200,8 @@ describe('Renderer', () => { const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({ - name: 'A', - decorators: [jasmine.objectContaining({name: 'Directive'})], + name: _('A'), + decorators: [jasmine.objectContaining({name: _('Directive')})] })); expect(addDefinitionsSpy.calls.first().args[2]) .toEqual( @@ -259,15 +259,15 @@ describe('Renderer', () => { it('should merge any external source map from the original file and write the output to an external source map', () => { - // Mock out reading the map file from disk - spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON()); + const sourceFiles = [{ + ...INPUT_PROGRAM, + contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map' + }]; + const mappingFiles = + [{name: INPUT_PROGRAM.name + '.map', contents: INPUT_PROGRAM_MAP.toJSON()}]; const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = - createTestRenderer( - 'test-package', [{ - ...INPUT_PROGRAM, - contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map' - }]); + createTestRenderer('test-package', sourceFiles, undefined, mappingFiles); const result = renderer.renderProgram( decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); @@ -275,7 +275,7 @@ describe('Renderer', () => { expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); expect(result[1].path).toEqual('/src/file.js.map'); - expect(result[1].contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON()); + expect(JSON.parse(result[1].contents)).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toObject()); }); }); diff --git a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts index c47474af37cd..f3c13a01dbaa 100644 --- a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts @@ -5,20 +5,16 @@ * 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} from 'fs'; -import * as mockFs from 'mock-fs'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {EntryPoint} from '../../src/packages/entry_point'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; import {InPlaceFileWriter} from '../../src/writing/in_place_file_writer'; +import {MockFileSystem} from '../helpers/mock_file_system'; const _ = AbsoluteFsPath.fromUnchecked; function createMockFileSystem() { - mockFs({ + return new MockFileSystem({ '/package/path': { 'top-level.js': 'ORIGINAL TOP LEVEL', 'folder-1': { @@ -34,16 +30,9 @@ function createMockFileSystem() { }); } -function restoreRealFileSystem() { - mockFs.restore(); -} - describe('InPlaceFileWriter', () => { - beforeEach(createMockFileSystem); - afterEach(restoreRealFileSystem); - it('should write all the FileInfo to the disk', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const fileWriter = new InPlaceFileWriter(fs); fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ {path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, @@ -51,16 +40,16 @@ describe('InPlaceFileWriter', () => { {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, ]); - expect(readFileSync('/package/path/top-level.js', 'utf8')).toEqual('MODIFIED TOP LEVEL'); - expect(readFileSync('/package/path/folder-1/file-1.js', 'utf8')).toEqual('MODIFIED FILE 1'); - expect(readFileSync('/package/path/folder-1/file-2.js', 'utf8')).toEqual('ORIGINAL FILE 2'); - expect(readFileSync('/package/path/folder-2/file-3.js', 'utf8')).toEqual('ORIGINAL FILE 3'); - expect(readFileSync('/package/path/folder-2/file-4.js', 'utf8')).toEqual('MODIFIED FILE 4'); - expect(readFileSync('/package/path/folder-3/file-5.js', 'utf8')).toEqual('NEW FILE 5'); + expect(fs.readFile(_('/package/path/top-level.js'))).toEqual('MODIFIED TOP LEVEL'); + expect(fs.readFile(_('/package/path/folder-1/file-1.js'))).toEqual('MODIFIED FILE 1'); + expect(fs.readFile(_('/package/path/folder-1/file-2.js'))).toEqual('ORIGINAL FILE 2'); + expect(fs.readFile(_('/package/path/folder-2/file-3.js'))).toEqual('ORIGINAL FILE 3'); + expect(fs.readFile(_('/package/path/folder-2/file-4.js'))).toEqual('MODIFIED FILE 4'); + expect(fs.readFile(_('/package/path/folder-3/file-5.js'))).toEqual('NEW FILE 5'); }); it('should create backups of all files that previously existed', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const fileWriter = new InPlaceFileWriter(fs); fileWriter.writeBundle({} as EntryPoint, {} as EntryPointBundle, [ {path: _('/package/path/top-level.js'), contents: 'MODIFIED TOP LEVEL'}, @@ -68,19 +57,19 @@ describe('InPlaceFileWriter', () => { {path: _('/package/path/folder-2/file-4.js'), contents: 'MODIFIED FILE 4'}, {path: _('/package/path/folder-3/file-5.js'), contents: 'NEW FILE 5'}, ]); - expect(readFileSync('/package/path/top-level.js.__ivy_ngcc_bak', 'utf8')) + expect(fs.readFile(_('/package/path/top-level.js.__ivy_ngcc_bak'))) .toEqual('ORIGINAL TOP LEVEL'); - expect(readFileSync('/package/path/folder-1/file-1.js.__ivy_ngcc_bak', 'utf8')) + expect(fs.readFile(_('/package/path/folder-1/file-1.js.__ivy_ngcc_bak'))) .toEqual('ORIGINAL FILE 1'); - expect(existsSync('/package/path/folder-1/file-2.js.__ivy_ngcc_bak')).toBe(false); - expect(existsSync('/package/path/folder-2/file-3.js.__ivy_ngcc_bak')).toBe(false); - expect(readFileSync('/package/path/folder-2/file-4.js.__ivy_ngcc_bak', 'utf8')) + expect(fs.exists(_('/package/path/folder-1/file-2.js.__ivy_ngcc_bak'))).toBe(false); + expect(fs.exists(_('/package/path/folder-2/file-3.js.__ivy_ngcc_bak'))).toBe(false); + expect(fs.readFile(_('/package/path/folder-2/file-4.js.__ivy_ngcc_bak'))) .toEqual('ORIGINAL FILE 4'); - expect(existsSync('/package/path/folder-3/file-5.js.__ivy_ngcc_bak')).toBe(false); + expect(fs.exists(_('/package/path/folder-3/file-5.js.__ivy_ngcc_bak'))).toBe(false); }); it('should error if the backup file already exists', () => { - const fs = new NodeJSFileSystem(); + const fs = createMockFileSystem(); const fileWriter = new InPlaceFileWriter(fs); expect( () => fileWriter.writeBundle( diff --git a/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts index 5775af81ffaf..051c85f08451 100644 --- a/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts @@ -5,24 +5,20 @@ * 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} from 'fs'; -import * as mockFs from 'mock-fs'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {FileSystem} from '../../src/file_system/file_system'; -import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo} from '../../src/packages/entry_point'; import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; import {FileWriter} from '../../src/writing/file_writer'; import {NewEntryPointFileWriter} from '../../src/writing/new_entry_point_file_writer'; +import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; import {loadPackageJson} from '../packages/entry_point_spec'; const _ = AbsoluteFsPath.from; function createMockFileSystem() { - mockFs({ + return new MockFileSystem({ '/node_modules/test': { 'package.json': '{"module": "./esm5.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', @@ -75,14 +71,7 @@ function createMockFileSystem() { }); } -function restoreRealFileSystem() { - mockFs.restore(); -} - describe('NewEntryPointFileWriter', () => { - beforeEach(createMockFileSystem); - afterEach(restoreRealFileSystem); - let fs: FileSystem; let fileWriter: FileWriter; let entryPoint: EntryPoint; @@ -91,7 +80,7 @@ describe('NewEntryPointFileWriter', () => { describe('writeBundle() [primary entry-point]', () => { beforeEach(() => { - fs = new NodeJSFileSystem(); + fs = createMockFileSystem(); fileWriter = new NewEntryPointFileWriter(fs); entryPoint = getEntryPointInfo( fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test')) !; @@ -107,14 +96,12 @@ describe('NewEntryPointFileWriter', () => { }, {path: _('/node_modules/test/esm5.js.map'), contents: 'MODIFIED MAPPING DATA'}, ]); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/esm5.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/esm5.js'))) .toEqual('export function FooTop() {} // MODIFIED'); - expect(readFileSync('/node_modules/test/esm5.js', 'utf8')) - .toEqual('export function FooTop() {}'); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/esm5.js.map', 'utf8')) + expect(fs.readFile(_('/node_modules/test/esm5.js'))).toEqual('export function FooTop() {}'); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/esm5.js.map'))) .toEqual('MODIFIED MAPPING DATA'); - expect(readFileSync('/node_modules/test/esm5.js.map', 'utf8')) - .toEqual('ORIGINAL MAPPING DATA'); + expect(fs.readFile(_('/node_modules/test/esm5.js.map'))).toEqual('ORIGINAL MAPPING DATA'); }); it('should also copy unmodified files in the program', () => { @@ -124,13 +111,12 @@ describe('NewEntryPointFileWriter', () => { contents: 'export class FooTop {} // MODIFIED' }, ]); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/es2015/foo.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/foo.js'))) .toEqual('export class FooTop {} // MODIFIED'); - expect(readFileSync('/node_modules/test/es2015/foo.js', 'utf8')) - .toEqual('export class FooTop {}'); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/es2015/index.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/es2015/foo.js'))).toEqual('export class FooTop {}'); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/es2015/index.js'))) .toEqual('export {FooTop} from "./foo";'); - expect(readFileSync('/node_modules/test/es2015/index.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/es2015/index.js'))) .toEqual('export {FooTop} from "./foo";'); }); @@ -141,7 +127,7 @@ describe('NewEntryPointFileWriter', () => { contents: 'export function FooTop() {} // MODIFIED' }, ]); - expect(loadPackageJson('/node_modules/test')).toEqual(jasmine.objectContaining({ + expect(loadPackageJson(fs, '/node_modules/test')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '__ivy_ngcc__/esm5.js', })); @@ -151,7 +137,7 @@ describe('NewEntryPointFileWriter', () => { contents: 'export class FooTop {} // MODIFIED' }, ]); - expect(loadPackageJson('/node_modules/test')).toEqual(jasmine.objectContaining({ + expect(loadPackageJson(fs, '/node_modules/test')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '__ivy_ngcc__/esm5.js', es2015_ivy_ngcc: '__ivy_ngcc__/es2015/index.js', })); @@ -161,27 +147,26 @@ describe('NewEntryPointFileWriter', () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ { path: _('/node_modules/test/index.d.ts'), - contents: 'export declare class FooTop {} // MODIFIED', + contents: 'export declare class FooTop {} // MODIFIED' }, {path: _('/node_modules/test/index.d.ts.map'), contents: 'MODIFIED MAPPING DATA'}, ]); - expect(readFileSync('/node_modules/test/index.d.ts', 'utf8')) + expect(fs.readFile(_('/node_modules/test/index.d.ts'))) .toEqual('export declare class FooTop {} // MODIFIED'); - expect(readFileSync('/node_modules/test/index.d.ts.__ivy_ngcc_bak', 'utf8')) + expect(fs.readFile(_('/node_modules/test/index.d.ts.__ivy_ngcc_bak'))) .toEqual('export declare class FooTop {}'); - expect(existsSync('/node_modules/test/__ivy_ngcc__/index.d.ts')).toBe(false); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts'))).toBe(false); - expect(readFileSync('/node_modules/test/index.d.ts.map', 'utf8')) - .toEqual('MODIFIED MAPPING DATA'); - expect(readFileSync('/node_modules/test/index.d.ts.map.__ivy_ngcc_bak', 'utf8')) + expect(fs.readFile(_('/node_modules/test/index.d.ts.map'))).toEqual('MODIFIED MAPPING DATA'); + expect(fs.readFile(_('/node_modules/test/index.d.ts.map.__ivy_ngcc_bak'))) .toEqual('ORIGINAL MAPPING DATA'); - expect(existsSync('/node_modules/test/__ivy_ngcc__/index.d.ts.map')).toBe(false); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/index.d.ts.map'))).toBe(false); }); }); describe('writeBundle() [secondary entry-point]', () => { beforeEach(() => { - fs = new NodeJSFileSystem(); + fs = createMockFileSystem(); fileWriter = new NewEntryPointFileWriter(fs); entryPoint = getEntryPointInfo( fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/a')) !; @@ -196,10 +181,9 @@ describe('NewEntryPointFileWriter', () => { contents: 'export function FooA() {} // MODIFIED' }, ]); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/a/esm5.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/esm5.js'))) .toEqual('export function FooA() {} // MODIFIED'); - expect(readFileSync('/node_modules/test/a/esm5.js', 'utf8')) - .toEqual('export function FooA() {}'); + expect(fs.readFile(_('/node_modules/test/a/esm5.js'))).toEqual('export function FooA() {}'); }); it('should also copy unmodified files in the program', () => { @@ -209,13 +193,12 @@ describe('NewEntryPointFileWriter', () => { contents: 'export class FooA {} // MODIFIED' }, ]); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/a/es2015/foo.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/foo.js'))) .toEqual('export class FooA {} // MODIFIED'); - expect(readFileSync('/node_modules/test/a/es2015/foo.js', 'utf8')) - .toEqual('export class FooA {}'); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/a/es2015/index.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/a/es2015/foo.js'))).toEqual('export class FooA {}'); + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/a/es2015/index.js'))) .toEqual('export {FooA} from "./foo";'); - expect(readFileSync('/node_modules/test/a/es2015/index.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/a/es2015/index.js'))) .toEqual('export {FooA} from "./foo";'); }); @@ -226,7 +209,7 @@ describe('NewEntryPointFileWriter', () => { contents: 'export function FooA() {} // MODIFIED' }, ]); - expect(loadPackageJson('/node_modules/test/a')).toEqual(jasmine.objectContaining({ + expect(loadPackageJson(fs, '/node_modules/test/a')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', })); @@ -236,7 +219,7 @@ describe('NewEntryPointFileWriter', () => { contents: 'export class FooA {} // MODIFIED' }, ]); - expect(loadPackageJson('/node_modules/test/a')).toEqual(jasmine.objectContaining({ + expect(loadPackageJson(fs, '/node_modules/test/a')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '../__ivy_ngcc__/a/esm5.js', es2015_ivy_ngcc: '../__ivy_ngcc__/a/es2015/index.js', })); @@ -249,17 +232,17 @@ describe('NewEntryPointFileWriter', () => { contents: 'export declare class FooA {} // MODIFIED' }, ]); - expect(readFileSync('/node_modules/test/a/index.d.ts', 'utf8')) + expect(fs.readFile(_('/node_modules/test/a/index.d.ts'))) .toEqual('export declare class FooA {} // MODIFIED'); - expect(readFileSync('/node_modules/test/a/index.d.ts.__ivy_ngcc_bak', 'utf8')) + expect(fs.readFile(_('/node_modules/test/a/index.d.ts.__ivy_ngcc_bak'))) .toEqual('export declare class FooA {}'); - expect(existsSync('/node_modules/test/__ivy_ngcc__/a/index.d.ts')).toBe(false); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/a/index.d.ts'))).toBe(false); }); }); describe('writeBundle() [entry-point (with files placed outside entry-point folder)]', () => { beforeEach(() => { - fs = new NodeJSFileSystem(); + fs = createMockFileSystem(); fileWriter = new NewEntryPointFileWriter(fs); entryPoint = getEntryPointInfo( fs, new MockLogger(), _('/node_modules/test'), _('/node_modules/test/b')) !; @@ -274,10 +257,9 @@ describe('NewEntryPointFileWriter', () => { contents: 'export function FooB() {} // MODIFIED' }, ]); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/lib/esm5.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/esm5.js'))) .toEqual('export function FooB() {} // MODIFIED'); - expect(readFileSync('/node_modules/test/lib/esm5.js', 'utf8')) - .toEqual('export function FooB() {}'); + expect(fs.readFile(_('/node_modules/test/lib/esm5.js'))).toEqual('export function FooB() {}'); }); it('should also copy unmodified files in the program', () => { @@ -287,13 +269,13 @@ describe('NewEntryPointFileWriter', () => { contents: 'export class FooB {} // MODIFIED' }, ]); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/lib/es2015/foo.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/es2015/foo.js'))) .toEqual('export class FooB {} // MODIFIED'); - expect(readFileSync('/node_modules/test/lib/es2015/foo.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/lib/es2015/foo.js'))) .toEqual('import {FooA} from "test/a"; import "events"; export class FooB {}'); - expect(readFileSync('/node_modules/test/__ivy_ngcc__/lib/es2015/index.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/__ivy_ngcc__/lib/es2015/index.js'))) .toEqual('export {FooB} from "./foo"; import * from "other";'); - expect(readFileSync('/node_modules/test/lib/es2015/index.js', 'utf8')) + expect(fs.readFile(_('/node_modules/test/lib/es2015/index.js'))) .toEqual('export {FooB} from "./foo"; import * from "other";'); }); @@ -301,11 +283,11 @@ describe('NewEntryPointFileWriter', () => { () => { fileWriter.writeBundle(entryPoint, esm2015bundle, [ { - path: '/node_modules/test/lib/es2015/foo.js', + path: _('/node_modules/test/lib/es2015/foo.js'), contents: 'export class FooB {} // MODIFIED' }, ]); - expect(existsSync('/node_modules/test/__ivy_ngcc__/a/index.d.ts')).toEqual(false); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/a/index.d.ts'))).toEqual(false); }); it('should not copy files outside of the package', () => { @@ -315,8 +297,8 @@ describe('NewEntryPointFileWriter', () => { contents: 'export class FooB {} // MODIFIED' }, ]); - expect(existsSync('/node_modules/test/other/index.d.ts')).toEqual(false); - expect(existsSync('/node_modules/test/events/events.js')).toEqual(false); + expect(fs.exists(_('/node_modules/test/other/index.d.ts'))).toEqual(false); + expect(fs.exists(_('/node_modules/test/events/events.js'))).toEqual(false); }); it('should update the package.json properties', () => { @@ -326,7 +308,7 @@ describe('NewEntryPointFileWriter', () => { contents: 'export function FooB() {} // MODIFIED' }, ]); - expect(loadPackageJson('/node_modules/test/b')).toEqual(jasmine.objectContaining({ + expect(loadPackageJson(fs, '/node_modules/test/b')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '../__ivy_ngcc__/lib/esm5.js', })); @@ -336,7 +318,7 @@ describe('NewEntryPointFileWriter', () => { contents: 'export class FooB {} // MODIFIED' }, ]); - expect(loadPackageJson('/node_modules/test/b')).toEqual(jasmine.objectContaining({ + expect(loadPackageJson(fs, '/node_modules/test/b')).toEqual(jasmine.objectContaining({ module_ivy_ngcc: '../__ivy_ngcc__/lib/esm5.js', es2015_ivy_ngcc: '../__ivy_ngcc__/lib/es2015/index.js', })); @@ -349,11 +331,11 @@ describe('NewEntryPointFileWriter', () => { contents: 'export declare class FooB {} // MODIFIED' }, ]); - expect(readFileSync('/node_modules/test/typings/index.d.ts', 'utf8')) + expect(fs.readFile(_('/node_modules/test/typings/index.d.ts'))) .toEqual('export declare class FooB {} // MODIFIED'); - expect(readFileSync('/node_modules/test/typings/index.d.ts.__ivy_ngcc_bak', 'utf8')) + expect(fs.readFile(_('/node_modules/test/typings/index.d.ts.__ivy_ngcc_bak'))) .toEqual('export declare class FooB {}'); - expect(existsSync('/node_modules/test/__ivy_ngcc__/typings/index.d.ts')).toBe(false); + expect(fs.exists(_('/node_modules/test/__ivy_ngcc__/typings/index.d.ts'))).toBe(false); }); }); }); From 6401a569cf3d3528a45aa37f779560bacd71a792 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:57 +0100 Subject: [PATCH 12/13] test(ivy): ngcc - tighten up typings in Esm5ReflectionHost specs --- packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 6f5430b93b6b..657b299b316b 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ClassMemberKind, Decorator, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {ClassMemberKind, CtorParameter, Decorator, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm5ReflectionHost, getIifeBody} from '../../src/host/esm5_host'; import {MockLogger} from '../helpers/mock_logger'; @@ -1184,7 +1184,7 @@ describe('Esm5ReflectionHost', () => { const parameters = host.getConstructorParameters(classNode); expect(parameters !.length).toBe(1); - expect(parameters ![0]).toEqual(jasmine.objectContaining({ + expect(parameters ![0]).toEqual(jasmine.objectContaining({ name: 'arg1', decorators: null, })); @@ -1200,11 +1200,11 @@ describe('Esm5ReflectionHost', () => { const parameters = host.getConstructorParameters(classNode); expect(parameters !.length).toBe(2); - expect(parameters ![0]).toEqual(jasmine.objectContaining({ + expect(parameters ![0]).toEqual(jasmine.objectContaining({ name: 'arg1', decorators: null, })); - expect(parameters ![1]).toEqual(jasmine.objectContaining({ + expect(parameters ![1]).toEqual(jasmine.objectContaining({ name: 'arg2', decorators: jasmine.any(Array) as any })); From e297d92c588d5a72fbbe6162ceeba89b1d8c0a15 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:47:57 +0100 Subject: [PATCH 13/13] refactor(ivy): ngcc - remove the last remnants of `path` and `canonical-path` The ngcc code now uses `AbsoluteFsPath` and `PathSegment` to do all its path manipulation. --- packages/compiler-cli/ngcc/main-ngcc.ts | 4 ++-- .../compiler-cli/ngcc/src/analysis/decoration_analyzer.ts | 3 +-- .../ngcc/src/dependencies/dependency_resolver.ts | 3 +-- packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts | 7 +++---- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/compiler-cli/ngcc/main-ngcc.ts b/packages/compiler-cli/ngcc/main-ngcc.ts index b4ec014e3728..e47904e58d9f 100644 --- a/packages/compiler-cli/ngcc/main-ngcc.ts +++ b/packages/compiler-cli/ngcc/main-ngcc.ts @@ -6,9 +6,9 @@ * 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 yargs from 'yargs'; +import {AbsoluteFsPath} from '../src/ngtsc/path'; import {mainNgcc} from './src/main'; import {ConsoleLogger, LogLevel} from './src/logging/console_logger'; @@ -56,7 +56,7 @@ if (require.main === module) { 'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.'); process.exit(1); } - const baseSourcePath = path.resolve(options['s'] || './node_modules'); + const baseSourcePath = AbsoluteFsPath.resolve(options['s'] || './node_modules'); const propertiesToConsider: string[] = options['p']; const targetEntryPointPath = options['t'] ? options['t'] : undefined; const compileAllFormats = !options['first-only']; diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index b69cb2eeab50..1a24bfab42e0 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ import {ConstantPool} from '@angular/compiler'; -import * as path from 'path'; import * as ts from 'typescript'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations'; @@ -57,7 +56,7 @@ class NgccResourceLoader implements ResourceLoader { preload(): undefined|Promise { throw new Error('Not implemented.'); } load(url: string): string { return this.fs.readFile(AbsoluteFsPath.resolve(url)); } resolve(url: string, containingFile: string): string { - return path.resolve(path.dirname(containingFile), url); + return AbsoluteFsPath.resolve(AbsoluteFsPath.dirname(AbsoluteFsPath.from(containingFile)), url); } } diff --git a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts index e289b5aabb79..f05424c7f02a 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts @@ -7,7 +7,6 @@ */ import {DepGraph} from 'dependency-graph'; -import {resolve} from 'path'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {Logger} from '../logging/logger'; @@ -171,7 +170,7 @@ function getEntryPointPath(entryPoint: EntryPoint): AbsoluteFsPath { if (format === 'esm2015' || format === 'esm5') { const formatPath = entryPoint.packageJson[property] !; - return AbsoluteFsPath.from(resolve(entryPoint.path, formatPath)); + return AbsoluteFsPath.resolve(entryPoint.path, formatPath); } } throw new Error(`There is no format with import statements in '${entryPoint.path}' entry-point.`); diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index f84dd23684a6..b6a54aaa2c47 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -9,7 +9,6 @@ import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; import {existsSync, readFileSync, readdirSync, statSync, writeFileSync} from 'fs'; import * as mockFs from 'mock-fs'; -import {join} from 'path'; import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../../../test/runfile_helpers'; import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; @@ -142,8 +141,8 @@ describe('ngcc main()', () => { function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) { - const basePath = '/node_modules'; - const targetPackageJsonPath = _(join(basePath, packagePath, 'package.json')); + const basePath = _('/node_modules'); + const targetPackageJsonPath = AbsoluteFsPath.join(basePath, packagePath, 'package.json'); const targetPackage = loadPackage(packagePath); const fs = new NodeJSFileSystem(); markAsProcessed(fs, targetPackage, targetPackageJsonPath, 'typings'); @@ -386,7 +385,7 @@ function loadDirectory(directoryPath: string): Directory { const directory: Directory = {}; readdirSync(directoryPath).forEach(item => { - const itemPath = join(directoryPath, item); + const itemPath = AbsoluteFsPath.resolve(directoryPath, item); if (statSync(itemPath).isDirectory()) { directory[item] = loadDirectory(itemPath); } else {