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:
parent
985cadb73d
commit
32a4a549fd
|
@ -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({
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue