diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 26bf7f6df0..959c0bbd97 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -19,7 +19,7 @@ import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTI import {Transformer} from '../../src/packages/transformer'; import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater'; import {MockLogger} from '../helpers/mock_logger'; -import {genNodeModules} from './util'; +import {compileIntoApf, compileIntoFlatEs5Package} from './util'; const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true}); @@ -113,23 +113,21 @@ runInEachFileSystem(() => { }); it('should generate correct metadata for decorated getter/setter properties', () => { - genNodeModules({ - 'test-package': { - '/index.ts': ` - import {Directive, Input, NgModule} from '@angular/core'; - - @Directive({selector: '[foo]'}) - export class FooDirective { - @Input() get bar() { return 'bar'; } - set bar(value: string) {} - } - - @NgModule({ - declarations: [FooDirective], - }) - export class FooModule {} - `, - }, + compileIntoFlatEs5Package('test-package', { + '/index.ts': ` + import {Directive, Input, NgModule} from '@angular/core'; + + @Directive({selector: '[foo]'}) + export class FooDirective { + @Input() get bar() { return 'bar'; } + set bar(value: string) {} + } + + @NgModule({ + declarations: [FooDirective], + }) + export class FooModule {} + `, }); mainNgcc({ @@ -148,24 +146,22 @@ runInEachFileSystem(() => { }); it('should not add `const` in ES5 generated code', () => { - genNodeModules({ - 'test-package': { - '/index.ts': ` - import {Directive, Input, NgModule} from '@angular/core'; - - @Directive({ - selector: '[foo]', - host: {bar: ''}, - }) - export class FooDirective { - } - - @NgModule({ - declarations: [FooDirective], - }) - export class FooModule {} - `, - }, + compileIntoFlatEs5Package('test-package', { + '/index.ts': ` + import {Directive, Input, NgModule} from '@angular/core'; + + @Directive({ + selector: '[foo]', + host: {bar: ''}, + }) + export class FooDirective { + } + + @NgModule({ + declarations: [FooDirective], + }) + export class FooModule {} + `, }); mainNgcc({ @@ -823,9 +819,8 @@ runInEachFileSystem(() => { describe('undecorated child class migration', () => { it('should generate a directive definition with CopyDefinitionFeature for an undecorated child directive', () => { - genNodeModules({ - 'test-package': { - '/index.ts': ` + compileIntoFlatEs5Package('test-package', { + '/index.ts': ` import {Directive, NgModule} from '@angular/core'; @Directive({ @@ -840,7 +835,6 @@ runInEachFileSystem(() => { }) export class Module {} `, - }, }); mainNgcc({ @@ -864,9 +858,8 @@ runInEachFileSystem(() => { it('should generate a component definition with CopyDefinitionFeature for an undecorated child component', () => { - genNodeModules({ - 'test-package': { - '/index.ts': ` + compileIntoFlatEs5Package('test-package', { + '/index.ts': ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -882,7 +875,6 @@ runInEachFileSystem(() => { }) export class Module {} `, - }, }); mainNgcc({ @@ -906,9 +898,8 @@ runInEachFileSystem(() => { it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain', () => { - genNodeModules({ - 'test-package': { - '/index.ts': ` + compileIntoFlatEs5Package('test-package', { + '/index.ts': ` import {Directive, NgModule} from '@angular/core'; @Directive({ @@ -927,7 +918,6 @@ runInEachFileSystem(() => { }) export class Module {} `, - }, }); mainNgcc({ diff --git a/packages/compiler-cli/ngcc/test/integration/util.ts b/packages/compiler-cli/ngcc/test/integration/util.ts index a0271d9b0f..0f5bd9161f 100644 --- a/packages/compiler-cli/ngcc/test/integration/util.ts +++ b/packages/compiler-cli/ngcc/test/integration/util.ts @@ -13,65 +13,75 @@ import {MockFileSystemPosix} from '../../../src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '../../../test/helpers'; -export type NodeModulesDef = { - [name: string]: Package -}; - -export type Package = { +export type PackageSources = { [path: string]: string; }; /** - * Compile one or more testing packages into the top-level `FileSystem`. + * Instead of writing packaged code by hand, and manually describing the layout of the package, this + * function transpiles the TypeScript sources into a flat file structure using the ES5 format. In + * this package layout, all compiled sources are at the root of the package, with .d.ts files next + * to the .js files. Each .js also has a corresponding .metadata.js file alongside with it. * - * Instead of writing ESM5 code by hand, and manually describing the Angular Package Format - * structure of that code in a mock NPM package, `genNodeModules` allows for the generation of one - * or more NPM packages from TypeScript source code. Each named NPM package in `def` is - * independently transpiled with `compileNodeModuleToFs` and written into `node_modules` in the - * top-level filesystem, ready for use in testing ngcc. + * All generated code is written into the `node_modules` in the top-level filesystem, ready for use + * in testing ngcc. + * + * @param pkgName The name of the package to compile. + * @param sources The TypeScript sources to compile. */ -export function genNodeModules(def: NodeModulesDef): void { - const fs = getFileSystem(); - for (const pkgName of Object.keys(def)) { - compileNodeModuleToFs(fs, pkgName, def[pkgName]); - } +export function compileIntoFlatEs5Package(pkgName: string, sources: PackageSources): void { + compileIntoFlatPackage(pkgName, sources, { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.ESNext, + }); +} + +export interface FlatLayoutOptions { + /** + * The script target version to compile into. + */ + target: ts.ScriptTarget; + + /** + * The module kind to use in the compiled result. + */ + module: ts.ModuleKind; } /** - * Takes the TypeScript project defined in the `Package` structure, compiles it to ESM5, and sets it - * up as a package in `node_modules` in `fs`. + * Instead of writing packaged code by hand, and manually describing the layout of the package, this + * function transpiles the TypeScript sources into a flat file structure using a single format. In + * this package layout, all compiled sources are at the root of the package, with .d.ts files next + * to the .js files. Each .js also has a corresponding .metadata.js file alongside with it. * - * TODO(alxhub): over time, expand this to other bundle formats and make it more faithful to the - * shape of real NPM packages. + * All generated code is written into the `node_modules` in the top-level filesystem, ready for use + * in testing ngcc. + * + * @param pkgName The name of the package to compile. + * @param sources The TypeScript sources to compile. + * @param options Allows for configuration of how the sources are compiled. */ -function compileNodeModuleToFs(fs: FileSystem, pkgName: string, pkg: Package): void { - const compileFs = new MockFileSystemPosix(true); - compileFs.init(loadStandardTestFiles({fakeCore: false})); +function compileIntoFlatPackage( + pkgName: string, sources: PackageSources, options: FlatLayoutOptions): void { + const fs = getFileSystem(); + const {rootNames, compileFs} = setupCompileFs(sources); - const options: ts.CompilerOptions = { - declaration: true, - module: ts.ModuleKind.ESNext, - target: ts.ScriptTarget.ES5, - lib: [], + const emit = (options: ts.CompilerOptions) => { + const host = new MockCompilerHost(compileFs); + const program = ts.createProgram({host, rootNames, options}); + program.emit(); }; - const rootNames = Object.keys(pkg); - - for (const fileName of rootNames) { - compileFs.writeFile(compileFs.resolve(fileName), pkg[fileName]); - } - - const host = new MockCompilerHost(compileFs); - const program = ts.createProgram({host, rootNames, options}); - program.emit(); + emit({declaration: true, module: options.module, target: options.target, lib: []}); // Copy over the JS and .d.ts files, and add a .metadata.json for each .d.ts file. - for (const inFileTs of rootNames) { - const inFileBase = inFileTs.replace(/\.ts$/, ''); - fs.writeFile( - fs.resolve(`/node_modules/${pkgName}/${inFileBase}.d.ts`), - compileFs.readFile(compileFs.resolve(`${inFileBase}.d.ts`))); - const jsContents = compileFs.readFile(compileFs.resolve(`${inFileBase}.js`)); + for (const file of rootNames) { + const inFileBase = file.replace(/\.ts$/, ''); + + const dtsContents = compileFs.readFile(compileFs.resolve(`/${inFileBase}.d.ts`)); + fs.writeFile(fs.resolve(`/node_modules/${pkgName}/${inFileBase}.d.ts`), dtsContents); + + const jsContents = compileFs.readFile(compileFs.resolve(`/${inFileBase}.js`)); fs.writeFile(fs.resolve(`/node_modules/${pkgName}/${inFileBase}.js`), jsContents); fs.writeFile(fs.resolve(`/node_modules/${pkgName}/${inFileBase}.metadata.json`), '{}'); } @@ -88,6 +98,109 @@ function compileNodeModuleToFs(fs: FileSystem, pkgName: string, pkg: Package): v fs.resolve(`/node_modules/${pkgName}/package.json`), JSON.stringify(pkgJson, null, 2)); } +/** + * Instead of writing packaged code by hand, and manually describing the layout of the package, this + * function transpiles the TypeScript sources into a package layout that of Angular Package Format. + * Both esm2015 and esm5 bundles are present in this layout. The .d.ts files reside in the /src + * directory and a public .d.ts file is present in the root, re-exporting /src/index.ts. + * + * Flat modules (fesm2015 and fesm5) and UMD bundles are not generated like they ought to be in APF. + * + * All generated code is written into the `node_modules` in the top-level filesystem, ready for use + * in testing ngcc. + */ +export function compileIntoApf(pkgName: string, sources: PackageSources): void { + const fs = getFileSystem(); + const {rootNames, compileFs} = setupCompileFs(sources); + + const emit = (options: ts.CompilerOptions) => { + const host = new MockCompilerHost(compileFs); + const program = ts.createProgram({host, rootNames, options}); + program.emit(); + }; + + // Compile esm2015 into /esm2015 + compileFs.ensureDir(compileFs.resolve('esm2015')); + emit({ + declaration: true, + outDir: './esm2015', + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES2015, + lib: [], + }); + + fs.ensureDir(fs.resolve(`/node_modules/${pkgName}/src`)); + fs.ensureDir(fs.resolve(`/node_modules/${pkgName}/esm2015/src`)); + for (const file of rootNames) { + const inFileBase = file.replace(/\.ts$/, ''); + + // Copy declaration file into /src tree + const dtsContents = compileFs.readFile(compileFs.resolve(`/esm2015/${inFileBase}.d.ts`)); + fs.writeFile(fs.resolve(`/node_modules/${pkgName}/src/${inFileBase}.d.ts`), dtsContents); + + // Copy compiled source file into /esm2015/src tree + const jsContents = compileFs.readFile(compileFs.resolve(`/esm2015/${inFileBase}.js`)); + fs.writeFile(fs.resolve(`/node_modules/${pkgName}/esm2015/src/${inFileBase}.js`), jsContents); + } + fs.writeFile( + fs.resolve(`/node_modules/${pkgName}/esm2015/index.js`), `export * from './src/index';`); + + // Compile esm5 into /esm5 + compileFs.ensureDir(compileFs.resolve('esm5')); + emit({ + declaration: false, + outDir: './esm5', + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES5, + lib: [], + }); + + fs.ensureDir(fs.resolve(`/node_modules/${pkgName}/esm5/src`)); + for (const file of rootNames) { + const inFileBase = file.replace(/\.ts$/, ''); + + // Copy compiled source file into esm5/src tree + const jsContents = compileFs.readFile(compileFs.resolve(`/esm5/${inFileBase}.js`)); + fs.writeFile(fs.resolve(`/node_modules/${pkgName}/esm5/src/${inFileBase}.js`), jsContents); + } + fs.writeFile( + fs.resolve(`/node_modules/${pkgName}/esm5/index.js`), `export * from './src/index';`); + + // Write a main declaration and metadata file to the root + fs.writeFile(fs.resolve(`/node_modules/${pkgName}/index.d.ts`), `export * from './src/index';`); + fs.writeFile(fs.resolve(`/node_modules/${pkgName}/index.metadata.json`), '{}'); + + // Write the package.json + const pkgJson: unknown = { + name: pkgName, + version: '0.0.1', + esm5: './esm5/index.js', + esm2015: './esm2015/index.js', + module: './esm5/index.js', + typings: './index.d.ts', + }; + + fs.writeFile( + fs.resolve(`/node_modules/${pkgName}/package.json`), JSON.stringify(pkgJson, null, 2)); +} + +/** + * Prepares a mock filesystem that contains all provided source files, which can be used to emit + * compiled code into. + */ +function setupCompileFs(sources: PackageSources): {rootNames: string[], compileFs: FileSystem} { + const compileFs = new MockFileSystemPosix(true); + compileFs.init(loadStandardTestFiles({fakeCore: false})); + + const rootNames = Object.keys(sources); + + for (const fileName of rootNames) { + compileFs.writeFile(compileFs.resolve(fileName), sources[fileName]); + } + + return {rootNames, compileFs}; +} + /** * A simple `ts.CompilerHost` that uses a `FileSystem` instead of the real FS. *