test(ngcc): expand integration tests with APF like package layouts (#33875)

ngcc has a basic integration test infrastructure that downlevels
TypeScript code into bundle formats that need to be processed by ngcc.
Until now, only ES5 bundles were created with a flat structure, however
more complex scenarios require an APF-like layout containing multiple
bundle formats.

PR Close #33875
This commit is contained in:
JoostK 2019-11-16 21:02:48 +01:00 committed by Alex Rickabaugh
parent 985cadb73d
commit 32a4a549fd
2 changed files with 194 additions and 91 deletions

View File

@ -19,7 +19,7 @@ import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTI
import {Transformer} from '../../src/packages/transformer'; import {Transformer} from '../../src/packages/transformer';
import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater'; import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {genNodeModules} from './util'; import {compileIntoApf, compileIntoFlatEs5Package} from './util';
const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true}); const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true});
@ -113,23 +113,21 @@ runInEachFileSystem(() => {
}); });
it('should generate correct metadata for decorated getter/setter properties', () => { it('should generate correct metadata for decorated getter/setter properties', () => {
genNodeModules({ compileIntoFlatEs5Package('test-package', {
'test-package': { '/index.ts': `
'/index.ts': ` import {Directive, Input, NgModule} from '@angular/core';
import {Directive, Input, NgModule} from '@angular/core';
@Directive({selector: '[foo]'})
@Directive({selector: '[foo]'}) export class FooDirective {
export class FooDirective { @Input() get bar() { return 'bar'; }
@Input() get bar() { return 'bar'; } set bar(value: string) {}
set bar(value: string) {} }
}
@NgModule({
@NgModule({ declarations: [FooDirective],
declarations: [FooDirective], })
}) export class FooModule {}
export class FooModule {} `,
`,
},
}); });
mainNgcc({ mainNgcc({
@ -148,24 +146,22 @@ runInEachFileSystem(() => {
}); });
it('should not add `const` in ES5 generated code', () => { it('should not add `const` in ES5 generated code', () => {
genNodeModules({ compileIntoFlatEs5Package('test-package', {
'test-package': { '/index.ts': `
'/index.ts': ` import {Directive, Input, NgModule} from '@angular/core';
import {Directive, Input, NgModule} from '@angular/core';
@Directive({
@Directive({ selector: '[foo]',
selector: '[foo]', host: {bar: ''},
host: {bar: ''}, })
}) export class FooDirective {
export class FooDirective { }
}
@NgModule({
@NgModule({ declarations: [FooDirective],
declarations: [FooDirective], })
}) export class FooModule {}
export class FooModule {} `,
`,
},
}); });
mainNgcc({ mainNgcc({
@ -823,9 +819,8 @@ runInEachFileSystem(() => {
describe('undecorated child class migration', () => { describe('undecorated child class migration', () => {
it('should generate a directive definition with CopyDefinitionFeature for an undecorated child directive', it('should generate a directive definition with CopyDefinitionFeature for an undecorated child directive',
() => { () => {
genNodeModules({ compileIntoFlatEs5Package('test-package', {
'test-package': { '/index.ts': `
'/index.ts': `
import {Directive, NgModule} from '@angular/core'; import {Directive, NgModule} from '@angular/core';
@Directive({ @Directive({
@ -840,7 +835,6 @@ runInEachFileSystem(() => {
}) })
export class Module {} export class Module {}
`, `,
},
}); });
mainNgcc({ mainNgcc({
@ -864,9 +858,8 @@ runInEachFileSystem(() => {
it('should generate a component definition with CopyDefinitionFeature for an undecorated child component', it('should generate a component definition with CopyDefinitionFeature for an undecorated child component',
() => { () => {
genNodeModules({ compileIntoFlatEs5Package('test-package', {
'test-package': { '/index.ts': `
'/index.ts': `
import {Component, NgModule} from '@angular/core'; import {Component, NgModule} from '@angular/core';
@Component({ @Component({
@ -882,7 +875,6 @@ runInEachFileSystem(() => {
}) })
export class Module {} export class Module {}
`, `,
},
}); });
mainNgcc({ mainNgcc({
@ -906,9 +898,8 @@ runInEachFileSystem(() => {
it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain', it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain',
() => { () => {
genNodeModules({ compileIntoFlatEs5Package('test-package', {
'test-package': { '/index.ts': `
'/index.ts': `
import {Directive, NgModule} from '@angular/core'; import {Directive, NgModule} from '@angular/core';
@Directive({ @Directive({
@ -927,7 +918,6 @@ runInEachFileSystem(() => {
}) })
export class Module {} export class Module {}
`, `,
},
}); });
mainNgcc({ mainNgcc({

View File

@ -13,65 +13,75 @@ import {MockFileSystemPosix} from '../../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../../test/helpers'; import {loadStandardTestFiles} from '../../../test/helpers';
export type NodeModulesDef = { export type PackageSources = {
[name: string]: Package
};
export type Package = {
[path: string]: string; [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 * All generated code is written into the `node_modules` in the top-level filesystem, ready for use
* structure of that code in a mock NPM package, `genNodeModules` allows for the generation of one * in testing ngcc.
* 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 * @param pkgName The name of the package to compile.
* top-level filesystem, ready for use in testing ngcc. * @param sources The TypeScript sources to compile.
*/ */
export function genNodeModules(def: NodeModulesDef): void { export function compileIntoFlatEs5Package(pkgName: string, sources: PackageSources): void {
const fs = getFileSystem(); compileIntoFlatPackage(pkgName, sources, {
for (const pkgName of Object.keys(def)) { target: ts.ScriptTarget.ES5,
compileNodeModuleToFs(fs, pkgName, def[pkgName]); 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 * Instead of writing packaged code by hand, and manually describing the layout of the package, this
* up as a package in `node_modules` in `fs`. * 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 * All generated code is written into the `node_modules` in the top-level filesystem, ready for use
* shape of real NPM packages. * 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 { function compileIntoFlatPackage(
const compileFs = new MockFileSystemPosix(true); pkgName: string, sources: PackageSources, options: FlatLayoutOptions): void {
compileFs.init(loadStandardTestFiles({fakeCore: false})); const fs = getFileSystem();
const {rootNames, compileFs} = setupCompileFs(sources);
const options: ts.CompilerOptions = { const emit = (options: ts.CompilerOptions) => {
declaration: true, const host = new MockCompilerHost(compileFs);
module: ts.ModuleKind.ESNext, const program = ts.createProgram({host, rootNames, options});
target: ts.ScriptTarget.ES5, program.emit();
lib: [],
}; };
const rootNames = Object.keys(pkg); emit({declaration: true, module: options.module, target: options.target, lib: []});
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();
// Copy over the JS and .d.ts files, and add a .metadata.json for each .d.ts file. // Copy over the JS and .d.ts files, and add a .metadata.json for each .d.ts file.
for (const inFileTs of rootNames) { for (const file of rootNames) {
const inFileBase = inFileTs.replace(/\.ts$/, ''); const inFileBase = file.replace(/\.ts$/, '');
fs.writeFile(
fs.resolve(`/node_modules/${pkgName}/${inFileBase}.d.ts`), const dtsContents = compileFs.readFile(compileFs.resolve(`/${inFileBase}.d.ts`));
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`));
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}.js`), jsContents);
fs.writeFile(fs.resolve(`/node_modules/${pkgName}/${inFileBase}.metadata.json`), '{}'); 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)); 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. * A simple `ts.CompilerHost` that uses a `FileSystem` instead of the real FS.
* *