refactor(tsc-wrapped): collect all exported functions and classes and bump metadata version from 1 to 2

This is needed to resolve symbols without `.d.ts` files.
This bumps the version of the metadata from 1 to 2.
This adds logic into `ng_host.ts` to automatically upgrade
version 1 to version 2 metadata by adding the exported symbols
from the `.d.ts` file.
This commit is contained in:
Tobias Bosch 2016-11-17 09:52:38 -08:00 committed by Chuck Jazdzewski
parent bccf0e69dc
commit dddbb1c1cb
9 changed files with 169 additions and 92 deletions

View File

@ -31,6 +31,8 @@ export class NgHost implements AotCompilerHost {
private isGenDirChildOfRootDir: boolean;
protected basePath: string;
private genDir: string;
private resolverCache = new Map<string, ModuleMetadata[]>();
constructor(
protected program: ts.Program, protected compilerHost: ts.CompilerHost,
protected options: AngularCompilerOptions, context?: NgHostContext) {
@ -138,9 +140,18 @@ export class NgHost implements AotCompilerHost {
}
}
private resolverCache = new Map<string, ModuleMetadata>();
protected getSourceFile(filePath: string): ts.SourceFile {
const sf = this.program.getSourceFile(filePath);
if (!sf) {
if (this.context.fileExists(filePath)) {
const sourceText = this.context.readFile(filePath);
return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
}
throw new Error(`Source file ${filePath} not present in program.`);
}
}
getMetadataFor(filePath: string): ModuleMetadata {
getMetadataFor(filePath: string): ModuleMetadata[] {
if (!this.context.fileExists(filePath)) {
// If the file doesn't exists then we cannot return metadata for the file.
// This will occur if the user refernced a declared module for which no file
@ -150,27 +161,51 @@ export class NgHost implements AotCompilerHost {
if (DTS.test(filePath)) {
const metadataPath = filePath.replace(DTS, '.metadata.json');
if (this.context.fileExists(metadataPath)) {
const metadata = this.readMetadata(metadataPath);
return (Array.isArray(metadata) && metadata.length == 0) ? undefined : metadata;
return this.readMetadata(metadataPath, filePath);
}
} else {
const sf = this.program.getSourceFile(filePath);
if (!sf) {
if (this.context.fileExists(filePath)) {
const sourceText = this.context.readFile(filePath);
return this.metadataCollector.getMetadata(
ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true));
}
throw new Error(`Source file ${filePath} not present in program.`);
}
return this.metadataCollector.getMetadata(sf);
const sf = this.getSourceFile(filePath);
const metadata = this.metadataCollector.getMetadata(sf);
return metadata ? [metadata] : [];
}
}
readMetadata(filePath: string) {
readMetadata(filePath: string, dtsFilePath: string): ModuleMetadata[] {
let metadatas = this.resolverCache.get(filePath);
if (metadatas) {
return metadatas;
}
try {
return this.resolverCache.get(filePath) || JSON.parse(this.context.readFile(filePath));
const metadataOrMetadatas = JSON.parse(this.context.readFile(filePath));
const metadatas = metadataOrMetadatas ?
(Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) :
[];
const v1Metadata = metadatas.find(m => m['version'] === 1);
let v2Metadata = metadatas.find(m => m['version'] === 2);
if (!v2Metadata && v1Metadata) {
// patch up v1 to v2 by merging the metadata with metadata collected from the d.ts file
// as the only difference between the versions is whether all exports are contained in
// the metadata
v2Metadata = {'__symbolic': 'module', 'version': 2, 'metadata': {}};
if (v1Metadata.exports) {
v2Metadata.exports = v1Metadata.exports;
}
for (let prop in v1Metadata.metadata) {
v2Metadata.metadata[prop] = v1Metadata.metadata[prop];
}
const sourceText = this.context.readFile(dtsFilePath);
const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath));
if (exports) {
for (let prop in exports.metadata) {
if (!v2Metadata.metadata[prop]) {
v2Metadata.metadata[prop] = exports.metadata[prop];
}
}
}
metadatas.push(v2Metadata);
}
this.resolverCache.set(filePath, metadatas);
return metadatas;
} catch (e) {
console.error(`Failed to read JSON file ${filePath}`);
throw e;
@ -178,15 +213,6 @@ export class NgHost implements AotCompilerHost {
}
loadResource(filePath: string): Promise<string> { return this.context.readResource(filePath); }
private getResolverMetadata(filePath: string): ModuleMetadata {
let metadata = this.resolverCache.get(filePath);
if (!metadata) {
metadata = this.getMetadataFor(filePath);
this.resolverCache.set(filePath, metadata);
}
return metadata;
}
}
export class NodeNgHostContext implements NgHostContext {

View File

@ -116,7 +116,7 @@ export class PathMappedNgHost extends NgHost {
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
}
getMetadataFor(filePath: string): ModuleMetadata {
getMetadataFor(filePath: string): ModuleMetadata[] {
for (const root of this.options.rootDirs || []) {
const rootedPath = path.join(root, filePath);
if (!this.compilerHost.fileExists(rootedPath)) {
@ -128,16 +128,13 @@ export class PathMappedNgHost extends NgHost {
if (DTS.test(rootedPath)) {
const metadataPath = rootedPath.replace(DTS, '.metadata.json');
if (this.context.fileExists(metadataPath)) {
const metadata = this.readMetadata(metadataPath);
return (Array.isArray(metadata) && metadata.length == 0) ? undefined : metadata;
return this.readMetadata(metadataPath, rootedPath);
}
} else {
const sf = this.program.getSourceFile(rootedPath);
if (!sf) {
throw new Error(`Source file ${rootedPath} not present in program.`);
}
const sf = this.getSourceFile(rootedPath);
sf.fileName = this.getCanonicalFileName(sf.fileName);
return this.metadataCollector.getMetadata(sf);
const metadata = this.metadataCollector.getMetadata(sf);
return metadata ? [metadata] : [];
}
}
}

View File

@ -141,8 +141,9 @@ describe('NgHost', () => {
});
it('should be able to read a metadata file', () => {
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/core.d.ts'))
.toEqual({__symbolic: 'module', version: 1, metadata: {foo: {__symbolic: 'class'}}});
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/core.d.ts')).toEqual([
{__symbolic: 'module', version: 2, metadata: {foo: {__symbolic: 'class'}}}
]);
});
it('should be able to read metadata from an otherwise unused .d.ts file ', () => {
@ -150,12 +151,22 @@ describe('NgHost', () => {
});
it('should be able to read empty metadata ', () => {
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/empty.d.ts')).toBeUndefined();
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/empty.d.ts')).toEqual([]);
});
it('should return undefined for missing modules', () => {
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/missing.d.ts')).toBeUndefined();
});
it('should add missing v2 metadata from v1 metadata and .d.ts files', () => {
expect(hostNestedGenDir.getMetadataFor('metadata_versions/v1.d.ts')).toEqual([
{__symbolic: 'module', version: 1, metadata: {foo: {__symbolic: 'class'}}}, {
__symbolic: 'module',
version: 2,
metadata: {foo: {__symbolic: 'class'}, bar: {__symbolic: 'class'}}
}
]);
});
});
const dummyModule = 'export let foo: any[];';
@ -179,12 +190,17 @@ const FILES: Entry = {
'@angular': {
'core.d.ts': dummyModule,
'core.metadata.json':
`{"__symbolic":"module", "version": 1, "metadata": {"foo": {"__symbolic": "class"}}}`,
`{"__symbolic":"module", "version": 2, "metadata": {"foo": {"__symbolic": "class"}}}`,
'router': {'index.d.ts': dummyModule, 'src': {'providers.d.ts': dummyModule}},
'unused.d.ts': dummyModule,
'empty.d.ts': 'export declare var a: string;',
'empty.metadata.json': '[]',
}
},
'metadata_versions': {
'v1.d.ts': 'export declare class bar {}',
'v1.metadata.json':
`{"__symbolic":"module", "version": 1, "metadata": {"foo": {"__symbolic": "class"}}}`,
}
}
}

View File

@ -22,7 +22,7 @@ export interface AotCompilerHost {
* @param modulePath is a string identifier for a module as an absolute path.
* @returns the metadata for the given module.
*/
getMetadataFor(modulePath: string): {[key: string]: any}|{[key: string]: any}[];
getMetadataFor(modulePath: string): {[key: string]: any}[];
/**
* Converts an import into a file path.

View File

@ -11,7 +11,7 @@ import {ReflectorReader} from '../private_import_core';
import {AotCompilerHost} from './compiler_host';
import {StaticSymbol} from './static_symbol';
const SUPPORTED_SCHEMA_VERSION = 1;
const SUPPORTED_SCHEMA_VERSION = 2;
const ANGULAR_IMPORT_LOCATIONS = {
coreDecorators: '@angular/core/src/metadata',
diDecorators: '@angular/core/src/di/metadata',
@ -604,10 +604,15 @@ export class StaticReflector implements ReflectorReader {
public getModuleMetadata(module: string): {[key: string]: any} {
let moduleMetadata = this.metadataCache.get(module);
if (!moduleMetadata) {
moduleMetadata = this.host.getMetadataFor(module);
if (Array.isArray(moduleMetadata)) {
moduleMetadata = moduleMetadata.find(md => md['version'] === SUPPORTED_SCHEMA_VERSION) ||
moduleMetadata[0];
const moduleMetadatas = this.host.getMetadataFor(module);
if (moduleMetadatas) {
let maxVersion = -1;
moduleMetadatas.forEach((md) => {
if (md['version'] > maxVersion) {
maxVersion = md['version'];
moduleMetadata = md;
}
});
}
if (!moduleMetadata) {
moduleMetadata =
@ -653,6 +658,7 @@ function expandedMessage(error: any): string {
if (error.context && error.context.name) {
return `Reference to a local (non-exported) symbol '${error.context.name}'. Consider exporting the symbol`;
}
break;
}
return error.message;
}

View File

@ -73,10 +73,10 @@ describe('StaticReflector', () => {
])]);
});
it('should throw and exception for unsupported metadata versions', () => {
it('should throw an exception for unsupported metadata versions', () => {
expect(() => reflector.findDeclaration('src/version-error', 'e'))
.toThrow(new Error(
'Metadata version mismatch for module /tmp/src/version-error.d.ts, found version 100, expected 1'));
'Metadata version mismatch for module /tmp/src/version-error.d.ts, found version 100, expected 2'));
});
it('should get and empty annotation list for an unknown class', () => {
@ -342,13 +342,11 @@ describe('StaticReflector', () => {
try {
const metadata = host.getMetadataFor('/tmp/src/invalid-metadata.ts');
expect(metadata).toBeDefined();
if (!Array.isArray(metadata)) {
const moduleMetadata: any = metadata['metadata'];
expect(moduleMetadata).toBeDefined();
const classData: any = moduleMetadata['InvalidMetadata'];
expect(classData).toBeDefined();
simplify(new StaticSymbol('/tmp/src/invalid-metadata.ts', ''), classData.decorators[0]);
}
const moduleMetadata: any = metadata[0]['metadata'];
expect(moduleMetadata).toBeDefined();
const classData: any = moduleMetadata['InvalidMetadata'];
expect(classData).toBeDefined();
simplify(new StaticSymbol('/tmp/src/invalid-metadata.ts', ''), classData.decorators[0]);
} catch (e) {
expect(e.fileName).toBe('/tmp/src/invalid-metadata.ts');
threw = true;
@ -376,7 +374,7 @@ describe('StaticReflector', () => {
const metadata = reflector.getModuleMetadata('/tmp/src/custom-decorator-reference.ts');
expect(metadata).toEqual({
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
Foo: {
__symbolic: 'class',
@ -564,7 +562,7 @@ class MockAotCompilerHost implements AotCompilerHost {
const data: {[key: string]: any} = {
'/tmp/@angular/common/src/forms-deprecated/directives.d.ts': [{
'__symbolic': 'module',
'version': 1,
'version': 2,
'metadata': {
'FORM_DIRECTIVES': [
{
@ -577,7 +575,7 @@ class MockAotCompilerHost implements AotCompilerHost {
}],
'/tmp/@angular/common/src/directives/ng_for.d.ts': {
'__symbolic': 'module',
'version': 1,
'version': 2,
'metadata': {
'NgFor': {
'__symbolic': 'class',
@ -630,16 +628,16 @@ class MockAotCompilerHost implements AotCompilerHost {
}
},
'/tmp/@angular/core/src/linker/view_container_ref.d.ts':
{version: 1, 'metadata': {'ViewContainerRef': {'__symbolic': 'class'}}},
{version: 2, 'metadata': {'ViewContainerRef': {'__symbolic': 'class'}}},
'/tmp/@angular/core/src/linker/template_ref.d.ts':
{version: 1, 'module': './template_ref', 'metadata': {'TemplateRef': {'__symbolic': 'class'}}},
{version: 2, 'module': './template_ref', 'metadata': {'TemplateRef': {'__symbolic': 'class'}}},
'/tmp/@angular/core/src/change_detection/differs/iterable_differs.d.ts':
{version: 1, 'metadata': {'IterableDiffers': {'__symbolic': 'class'}}},
{version: 2, 'metadata': {'IterableDiffers': {'__symbolic': 'class'}}},
'/tmp/@angular/core/src/change_detection/change_detector_ref.d.ts':
{version: 1, 'metadata': {'ChangeDetectorRef': {'__symbolic': 'class'}}},
{version: 2, 'metadata': {'ChangeDetectorRef': {'__symbolic': 'class'}}},
'/tmp/src/app/hero-detail.component.d.ts': {
'__symbolic': 'module',
'version': 1,
'version': 2,
'metadata': {
'HeroDetailComponent': {
'__symbolic': 'class',
@ -790,11 +788,11 @@ class MockAotCompilerHost implements AotCompilerHost {
}
}
},
'/src/extern.d.ts': {'__symbolic': 'module', 'version': 1, metadata: {s: 's'}},
'/src/extern.d.ts': {'__symbolic': 'module', 'version': 2, metadata: {s: 's'}},
'/tmp/src/version-error.d.ts': {'__symbolic': 'module', 'version': 100, metadata: {e: 's'}},
'/tmp/src/error-reporting.d.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
SomeClass: {
__symbolic: 'class',
@ -824,7 +822,7 @@ class MockAotCompilerHost implements AotCompilerHost {
},
'/tmp/src/error-references.d.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
Link1: {
__symbolic: 'reference',
@ -846,7 +844,7 @@ class MockAotCompilerHost implements AotCompilerHost {
},
'/tmp/src/function-declaration.d.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
one: {
__symbolic: 'function',
@ -875,7 +873,7 @@ class MockAotCompilerHost implements AotCompilerHost {
},
'/tmp/src/function-reference.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
one: {
__symbolic: 'call',
@ -917,7 +915,7 @@ class MockAotCompilerHost implements AotCompilerHost {
},
'/tmp/src/function-recursive.d.ts': {
__symbolic: 'modules',
version: 1,
version: 2,
metadata: {
recursive: {
__symbolic: 'function',
@ -977,7 +975,7 @@ class MockAotCompilerHost implements AotCompilerHost {
},
'/tmp/src/spread.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
spread: [0, {__symbolic: 'spread', expression: [1, 2, 3, 4]}, 5]
}
@ -1113,7 +1111,7 @@ class MockAotCompilerHost implements AotCompilerHost {
`,
'/tmp/src/reexport/reexport.d.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {},
exports: [
{from: './src/origin1', export: ['One', 'Two', {name: 'Three', as: 'Four'}]},
@ -1122,7 +1120,7 @@ class MockAotCompilerHost implements AotCompilerHost {
},
'/tmp/src/reexport/src/origin1.d.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
One: {__symbolic: 'class'},
Two: {__symbolic: 'class'},
@ -1131,26 +1129,26 @@ class MockAotCompilerHost implements AotCompilerHost {
},
'/tmp/src/reexport/src/origin5.d.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
Five: {__symbolic: 'class'},
},
},
'/tmp/src/reexport/src/origin30.d.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
Thirty: {__symbolic: 'class'},
},
},
'/tmp/src/reexport/src/originNone.d.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {},
},
'/tmp/src/reexport/src/reexport2.d.ts': {
__symbolic: 'module',
version: 1,
version: 2,
metadata: {},
exports: [{from: './originNone'}, {from: './origin30'}]
}
@ -1166,9 +1164,14 @@ class MockAotCompilerHost implements AotCompilerHost {
if (diagnostics && diagnostics.length) {
throw Error(`Error encountered during parse of file ${moduleId}`);
}
return this.collector.getMetadata(sf);
return [this.collector.getMetadata(sf)];
}
}
return data[moduleId];
const result = data[moduleId];
if (result) {
return Array.isArray(result) ? result : [result];
} else {
return null;
}
}
}

View File

@ -256,9 +256,11 @@ export class MetadataCollector {
if (classDeclaration.name) {
const className = classDeclaration.name.text;
if (node.flags & ts.NodeFlags.Export) {
if (!metadata) metadata = {};
if (classDeclaration.decorators) {
if (!metadata) metadata = {};
metadata[className] = classMetadataOf(classDeclaration);
} else {
metadata[className] = {__symbolic: 'class'};
}
}
}
@ -269,10 +271,14 @@ export class MetadataCollector {
// names substitution will be performed by the StaticReflector.
const functionDeclaration = <ts.FunctionDeclaration>node;
if (node.flags & ts.NodeFlags.Export) {
if (!metadata) metadata = {};
const maybeFunc = maybeGetSimpleFunction(functionDeclaration);
if (maybeFunc) {
if (!metadata) metadata = {};
metadata[maybeFunc.name] = recordEntry(maybeFunc.func, node);
} else if (functionDeclaration.name.kind == ts.SyntaxKind.Identifier) {
const nameNode = <ts.Identifier>functionDeclaration.name;
const functionName = nameNode.text;
metadata[functionName] = {__symbolic: 'function'};
}
}
break;

View File

@ -15,7 +15,7 @@
// semantics of the file in an array. For example, when generating a version 2 file, if version 1
// can accurately represent the metadata, generate both version 1 and version 2 in an array.
export const VERSION = 1;
export const VERSION = 2;
export type MetadataEntry = ClassMetadata | FunctionMetadata | MetadataValue;

View File

@ -29,6 +29,7 @@ describe('Collector', () => {
'/unsupported-1.ts',
'/unsupported-2.ts',
'import-star.ts',
'exported-classes.ts',
'exported-functions.ts',
'exported-enum.ts',
'exported-consts.ts',
@ -62,7 +63,7 @@ describe('Collector', () => {
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
HeroDetailComponent: {
__symbolic: 'class',
@ -103,7 +104,7 @@ describe('Collector', () => {
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
AppComponent: {
__symbolic: 'class',
@ -157,7 +158,7 @@ describe('Collector', () => {
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
HEROES: [
{'id': 11, 'name': 'Mr. Nice'}, {'id': 12, 'name': 'Narco'},
@ -170,13 +171,6 @@ describe('Collector', () => {
});
});
it('should return undefined for modules that have no metadata', () => {
const sourceFile = program.getSourceFile('/app/error-cases.ts');
expect(sourceFile).toBeTruthy(sourceFile);
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toBeUndefined();
});
let casesFile: ts.SourceFile;
let casesMetadata: ModuleMetadata;
@ -238,7 +232,7 @@ describe('Collector', () => {
const metadata = collector.getMetadata(unsupported1);
expect(metadata).toEqual({
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
a: {__symbolic: 'error', message: 'Destructuring not supported', line: 1, character: 16},
b: {__symbolic: 'error', message: 'Destructuring not supported', line: 1, character: 19},
@ -275,12 +269,26 @@ describe('Collector', () => {
]);
});
it('should record all exported classes', () => {
const sourceFile = program.getSourceFile('/exported-classes.ts');
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: 2,
metadata: {
SimpleClass: {__symbolic: 'class'},
AbstractClass: {__symbolic: 'class'},
DeclaredClass: {__symbolic: 'class'}
}
});
});
it('should be able to record functions', () => {
const exportedFunctions = program.getSourceFile('/exported-functions.ts');
const metadata = collector.getMetadata(exportedFunctions);
expect(metadata).toEqual({
__symbolic: 'module',
version: 1,
version: 2,
metadata: {
one: {
__symbolic: 'function',
@ -328,7 +336,9 @@ describe('Collector', () => {
}
}
}
}
},
complexFn: {__symbolic: 'function'},
declaredFn: {__symbolic: 'function'}
}
});
});
@ -829,6 +839,11 @@ const FILES: Directory = {
constructor(private f: common.NgFor) {}
}
`,
'exported-classes.ts': `
export class SimpleClass {}
export abstract class AbstractClass {}
export declare class DeclaredClass {}
`,
'exported-functions.ts': `
export function one(a: string, b: string, c: string) {
return {a: a, b: b, c: c};
@ -842,6 +857,14 @@ const FILES: Directory = {
export function supportsState(): boolean {
return !!window.history.pushState;
}
export function complexFn(x: any): boolean {
if (x) {
return true;
} else {
return false;
}
}
export declare function declaredFn();
`,
'exported-enum.ts': `
import {constValue} from './exported-consts';