angular-cn/packages/compiler-cli/test/metadata/bundler_spec.ts

536 lines
17 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as path from 'path';
import * as ts from 'typescript';
import {CompilerHostAdapter, MetadataBundler, MetadataBundlerHost} from '../../src/metadata/bundler';
import {MetadataCollector} from '../../src/metadata/collector';
import {ClassMetadata, MetadataGlobalReferenceExpression, ModuleMetadata} from '../../src/metadata/schema';
import {Directory, MockAotContext, MockCompilerHost} from '../mocks';
describe('compiler host adapter', () => {
it('should retrieve metadata for an explicit index relative path reference', () => {
const context = new MockAotContext('.', SIMPLE_LIBRARY);
const host = new MockCompilerHost(context);
const options: ts.CompilerOptions = {
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES5,
};
const adapter = new CompilerHostAdapter(host, null, options);
const metadata = adapter.getMetadataFor('./lib/src/two/index', '.');
expect(metadata).toBeDefined();
expect(Object.keys(metadata !.metadata).sort()).toEqual([
'PrivateTwo',
'TWO_CLASSES',
'Two',
'TwoMore',
]);
});
it('should retrieve metadata for an implied index relative path reference', () => {
const context = new MockAotContext('.', SIMPLE_LIBRARY_WITH_IMPLIED_INDEX);
const host = new MockCompilerHost(context);
const options: ts.CompilerOptions = {
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES5,
};
const adapter = new CompilerHostAdapter(host, null, options);
const metadata = adapter.getMetadataFor('./lib/src/two', '.');
expect(metadata).toBeDefined();
expect(Object.keys(metadata !.metadata).sort()).toEqual([
'PrivateTwo',
'TWO_CLASSES',
'Two',
'TwoMore',
]);
});
it('should fail to retrieve metadata for an implied index with classic module resolution', () => {
const context = new MockAotContext('.', SIMPLE_LIBRARY_WITH_IMPLIED_INDEX);
const host = new MockCompilerHost(context);
const options: ts.CompilerOptions = {
moduleResolution: ts.ModuleResolutionKind.Classic,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES5,
};
const adapter = new CompilerHostAdapter(host, null, options);
const metadata = adapter.getMetadataFor('./lib/src/two', '.');
expect(metadata).toBeUndefined();
});
it('should retrieve exports for an explicit index relative path reference', () => {
const context = new MockAotContext('.', SIMPLE_LIBRARY);
const host = new MockCompilerHost(context);
const options: ts.CompilerOptions = {
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES5,
};
const adapter = new CompilerHostAdapter(host, null, options);
const metadata = adapter.getMetadataFor('./lib/src/index', '.');
expect(metadata).toBeDefined();
expect(metadata !.exports !.map(e => e.export !)
.reduce((prev, next) => prev.concat(next), [])
.sort())
.toEqual([
'ONE_CLASSES',
'One',
'OneMore',
'TWO_CLASSES',
'Two',
'TwoMore',
]);
});
it('should look for .ts file when resolving metadata via a package.json "main" entry', () => {
const files = {
'lib': {
'one.ts': `
class One {}
class OneMore extends One {}
class PrivateOne {}
const ONE_CLASSES = [One, OneMore, PrivateOne];
export {One, OneMore, PrivateOne, ONE_CLASSES};
`,
'one.js': `
// This will throw an error if the metadata collector tries to load one.js
`,
'package.json': `
{
"main": "one"
}
`
}
};
const context = new MockAotContext('.', files);
const host = new MockCompilerHost(context);
const options: ts.CompilerOptions = {
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES5,
};
const adapter = new CompilerHostAdapter(host, null, options);
const metadata = adapter.getMetadataFor('./lib', '.');
expect(metadata).toBeDefined();
expect(Object.keys(metadata !.metadata).sort()).toEqual([
'ONE_CLASSES',
'One',
'OneMore',
'PrivateOne',
]);
expect(Array.isArray(metadata !.metadata !['ONE_CLASSES'])).toBeTruthy();
});
it('should look for non-declaration file when resolving metadata via a package.json "types" entry',
() => {
const files = {
'lib': {
'one.ts': `
class One {}
class OneMore extends One {}
class PrivateOne {}
const ONE_CLASSES = [One, OneMore, PrivateOne];
export {One, OneMore, PrivateOne, ONE_CLASSES};
`,
'one.d.ts': `
declare class One {
}
declare class OneMore extends One {
}
declare class PrivateOne {
}
declare const ONE_CLASSES: (typeof One)[];
export { One, OneMore, PrivateOne, ONE_CLASSES };
`,
'one.js': `
// This will throw an error if the metadata collector tries to load one.js
`,
'package.json': `
{
"main": "one",
"types": "one.d.ts"
}
`
}
};
const context = new MockAotContext('.', files);
const host = new MockCompilerHost(context);
const options: ts.CompilerOptions = {
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES5,
};
const adapter = new CompilerHostAdapter(host, null, options);
const metadata = adapter.getMetadataFor('./lib', '.');
expect(metadata).toBeDefined();
expect(Object.keys(metadata !.metadata).sort()).toEqual([
'ONE_CLASSES',
'One',
'OneMore',
'PrivateOne',
]);
expect(Array.isArray(metadata !.metadata !['ONE_CLASSES'])).toBeTruthy();
});
});
describe('metadata bundler', () => {
it('should be able to bundle a simple library', () => {
const host = new MockStringBundlerHost('/', SIMPLE_LIBRARY);
const bundler = new MetadataBundler('/lib/index', undefined, host, 'prfx_');
const result = bundler.getMetadataBundle();
expect(Object.keys(result.metadata.metadata).sort()).toEqual([
'ONE_CLASSES', 'One', 'OneMore', 'TWO_CLASSES', 'Two', 'TwoMore', 'ɵprfx_a', 'ɵprfx_b'
]);
const originalOne = './src/one';
const originalTwo = './src/two/index';
expect(Object.keys(result.metadata.origins !)
.sort()
.map(name => ({name, value: result.metadata.origins ![name]})))
.toEqual([
{name: 'ONE_CLASSES', value: originalOne}, {name: 'One', value: originalOne},
{name: 'OneMore', value: originalOne}, {name: 'TWO_CLASSES', value: originalTwo},
{name: 'Two', value: originalTwo}, {name: 'TwoMore', value: originalTwo},
{name: 'ɵprfx_a', value: originalOne}, {name: 'ɵprfx_b', value: originalTwo}
]);
expect(result.privates).toEqual([
{privateName: 'ɵprfx_a', name: 'PrivateOne', module: originalOne},
{privateName: 'ɵprfx_b', name: 'PrivateTwo', module: originalTwo}
]);
});
fix(compiler-cli): incorrect bundled metadata for static class member call expressions (#28762) Currently if developers use call expressions in their static class members ([like we do in Angular](https://github.com/angular/angular/blob/master/packages/core/src/change_detection/differs/keyvalue_differs.ts#L121)), the metadata that is generated for flat modules is invalid. This is because the metadata bundler logic currently does not handle call expressions in static class members and the symbol references are not rewritten to avoid relative paths in the bundle. Static class members using a call expression are not relevant for the ViewEngine AOT compilation, but it is problematic that the bundled metadata references modules using their original relative path. This means that the bundled metadata is no longer encapsulated and depends on other emitted files to be emitted in the proper place. These incorrect relative paths can now cause issues where NGC looks for the referenced symbols in the incorrect path. e.g. ``` src/ | lib/ | index.ts -> References the call expression using `../../di` ``` Now the metadata looks like that: ``` node_modules/ | @angular/ -- | core/ -- -- | core.metadata.json -> Says that the call expr. is in `../../di`. | di/ ``` Now if NGC tries to use the metadata files and create the summary files, NGC resolves the call expression to the `node_modules/di` module. Since the "unexpected" module does not contain the desired symbol, NGC will error out. We should fix this by ensuring that we don't ship corrupted metadata to NPM which contains relative references that can cause such failures (other imports can be affected as well; it depends on what modules the developer has installed and how we import our call expressions). Fixes #28741. PR Close #28762
2019-02-15 12:12:56 -05:00
it('should rewrite call expression references for static class members', () => {
const host = new MockStringBundlerHost('/', {
'lib': {
'index.ts': `export * from './deep/index';`,
'shared.ts': `
export function sharedFn() {
return {foo: true};
}`,
'deep': {
'index.ts': `
import {sharedFn} from '../shared';
export class MyClass {
static ngInjectableDef = sharedFn();
}
`,
}
}
});
const bundler = new MetadataBundler('/lib/index', undefined, host);
const bundledMetadata = bundler.getMetadataBundle().metadata;
const deepIndexMetadata = host.getMetadataFor('/lib/deep/index') !;
// The unbundled metadata should reference symbols using the relative module path.
expect(deepIndexMetadata.metadata['MyClass']).toEqual(jasmine.objectContaining({
statics: {
ngInjectableDef: {
__symbolic: 'call',
expression: {
__symbolic: 'reference',
name: 'sharedFn',
module: '../shared',
}
}
}
}));
// For the bundled metadata, the "sharedFn" symbol should not be referenced using the
// relative module path (like for unbundled), because the metadata bundle can be stored
// anywhere and it's not guaranteed that the relatively referenced files are present.
expect(bundledMetadata.metadata['MyClass']).toEqual(jasmine.objectContaining({
statics: {
ngInjectableDef: {
__symbolic: 'call',
expression: {
__symbolic: 'reference',
name: 'ɵa',
}
}
}
}));
});
it('should be able to bundle an oddly constructed library', () => {
const host = new MockStringBundlerHost('/', {
'lib': {
'index.ts': `
export * from './src/index';
`,
'src': {
'index.ts': `
export {One, OneMore, ONE_CLASSES} from './one';
export {Two, TwoMore, TWO_CLASSES} from './two/index';
`,
'one.ts': `
class One {}
class OneMore extends One {}
class PrivateOne {}
const ONE_CLASSES = [One, OneMore, PrivateOne];
export {One, OneMore, PrivateOne, ONE_CLASSES};
`,
'two': {
'index.ts': `
class Two {}
class TwoMore extends Two {}
class PrivateTwo {}
const TWO_CLASSES = [Two, TwoMore, PrivateTwo];
export {Two, TwoMore, PrivateTwo, TWO_CLASSES};
`
}
}
}
});
const bundler = new MetadataBundler('/lib/index', undefined, host);
const result = bundler.getMetadataBundle();
expect(Object.keys(result.metadata.metadata).sort()).toEqual([
'ONE_CLASSES', 'One', 'OneMore', 'TWO_CLASSES', 'Two', 'TwoMore', 'ɵa', 'ɵb'
]);
expect(result.privates).toEqual([
{privateName: 'ɵa', name: 'PrivateOne', module: './src/one'},
{privateName: 'ɵb', name: 'PrivateTwo', module: './src/two/index'}
]);
});
it('should not output windows paths in metadata', () => {
const host = new MockStringBundlerHost('/', {
'index.ts': `
export * from './exports/test';
`,
'exports': {'test.ts': `export class TestExport {}`}
});
const bundler = new MetadataBundler('/index', undefined, host);
const result = bundler.getMetadataBundle();
expect(result.metadata.origins).toEqual({'TestExport': './exports/test'});
});
it('should convert re-exported to the export', () => {
const host = new MockStringBundlerHost('/', {
'index.ts': `
export * from './bar';
export * from './foo';
`,
'bar.ts': `
import {Foo} from './foo';
export class Bar extends Foo {
}
`,
'foo.ts': `
export {Foo} from 'foo';
`
});
const bundler = new MetadataBundler('/index', undefined, host);
const result = bundler.getMetadataBundle();
// Expect the extends reference to refer to the imported module
expect((result.metadata.metadata as any).Bar.extends.module).toEqual('foo');
expect(result.privates).toEqual([]);
});
it('should treat import then export as a simple export', () => {
const host = new MockStringBundlerHost('/', {
'index.ts': `
export * from './a';
export * from './c';
`,
'a.ts': `
import { B } from './b';
export { B };
`,
'b.ts': `
export class B { }
`,
'c.ts': `
import { B } from './b';
export class C extends B { }
`
});
const bundler = new MetadataBundler('/index', undefined, host);
const result = bundler.getMetadataBundle();
expect(Object.keys(result.metadata.metadata).sort()).toEqual(['B', 'C']);
expect(result.privates).toEqual([]);
});
it('should be able to bundle a private from a un-exported module', () => {
const host = new MockStringBundlerHost('/', {
'index.ts': `
export * from './foo';
`,
'foo.ts': `
import {Bar} from './bar';
export class Foo extends Bar {
}
`,
'bar.ts': `
export class Bar {}
`
});
const bundler = new MetadataBundler('/index', undefined, host);
const result = bundler.getMetadataBundle();
expect(Object.keys(result.metadata.metadata).sort()).toEqual(['Foo', 'ɵa']);
expect(result.privates).toEqual([{privateName: 'ɵa', name: 'Bar', module: './bar'}]);
});
it('should be able to bundle a library with re-exported symbols', () => {
const host = new MockStringBundlerHost('/', {
'public-api.ts': `
export * from './src/core';
export * from './src/externals';
`,
'src': {
'core.ts': `
export class A {}
export class B extends A {}
`,
'externals.ts': `
export {E, F, G} from 'external_one';
export * from 'external_two';
`
}
});
const bundler = new MetadataBundler('/public-api', undefined, host);
const result = bundler.getMetadataBundle();
expect(result.metadata.exports).toEqual([
{from: 'external_two'}, {
export: [{name: 'E', as: 'E'}, {name: 'F', as: 'F'}, {name: 'G', as: 'G'}],
from: 'external_one'
}
]);
expect(result.metadata.origins !['E']).toBeUndefined();
});
it('should be able to de-duplicate symbols of re-exported modules', () => {
const host = new MockStringBundlerHost('/', {
'public-api.ts': `
export {A as A2, A, B as B1, B as B2} from './src/core';
export {A as A3} from './src/alternate';
`,
'src': {
'core.ts': `
export class A {}
export class B {}
`,
'alternate.ts': `
export class A {}
`,
}
});
const bundler = new MetadataBundler('/public-api', undefined, host);
const result = bundler.getMetadataBundle();
const {A, A2, A3, B1, B2} = result.metadata.metadata as{
A: ClassMetadata,
A2: MetadataGlobalReferenceExpression,
A3: ClassMetadata,
B1: ClassMetadata,
B2: MetadataGlobalReferenceExpression
};
expect(A.__symbolic).toEqual('class');
expect(A2.__symbolic).toEqual('reference');
expect(A2.name).toEqual('A');
expect(A3.__symbolic).toEqual('class');
expect(B1.__symbolic).toEqual('class');
expect(B2.__symbolic).toEqual('reference');
expect(B2.name).toEqual('B1');
});
});
export class MockStringBundlerHost implements MetadataBundlerHost {
collector = new MetadataCollector();
adapter: CompilerHostAdapter;
constructor(private dirName: string, directory: Directory) {
const context = new MockAotContext(dirName, directory);
const host = new MockCompilerHost(context);
const options = {
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES5,
};
this.adapter = new CompilerHostAdapter(host, null, options);
}
getMetadataFor(moduleName: string): ModuleMetadata|undefined {
return this.adapter.getMetadataFor(moduleName, this.dirName);
}
}
export const SIMPLE_LIBRARY = {
'lib': {
'index.ts': `
export * from './src/index';
`,
'src': {
'index.ts': `
export {One, OneMore, ONE_CLASSES} from './one';
export {Two, TwoMore, TWO_CLASSES} from './two/index';
`,
'one.ts': `
export class One {}
export class OneMore extends One {}
export class PrivateOne {}
export const ONE_CLASSES = [One, OneMore, PrivateOne];
`,
'two': {
'index.ts': `
export class Two {}
export class TwoMore extends Two {}
export class PrivateTwo {}
export const TWO_CLASSES = [Two, TwoMore, PrivateTwo];
`
}
}
}
};
export const SIMPLE_LIBRARY_WITH_IMPLIED_INDEX = {
'lib': {
'index.ts': `
export * from './src';
`,
'src': {
'index.ts': `
export {One, OneMore, ONE_CLASSES} from './one';
export {Two, TwoMore, TWO_CLASSES} from './two';
`,
'one.ts': `
export class One {}
export class OneMore extends One {}
export class PrivateOne {}
export const ONE_CLASSES = [One, OneMore, PrivateOne];
`,
'two': {
'index.ts': `
export class Two {}
export class TwoMore extends Two {}
export class PrivateTwo {}
export const TWO_CLASSES = [Two, TwoMore, PrivateTwo];
`
}
}
}
};