diff --git a/modules/@angular/compiler-cli/src/compiler_host.ts b/modules/@angular/compiler-cli/src/compiler_host.ts index f9d6a2ec20..5817134c2f 100644 --- a/modules/@angular/compiler-cli/src/compiler_host.ts +++ b/modules/@angular/compiler-cli/src/compiler_host.ts @@ -181,6 +181,12 @@ export class CompilerHost implements AotCompilerHost { const metadataPath = filePath.replace(DTS, '.metadata.json'); if (this.context.fileExists(metadataPath)) { return this.readMetadata(metadataPath, filePath); + } else { + // If there is a .d.ts file but no metadata file we need to produce a + // v3 metadata from the .d.ts file as v3 includes the exports we need + // to resolve symbols. + return [this.upgradeVersion1Metadata( + {'__symbolic': 'module', 'version': 1, 'metadata': {}}, filePath)]; } } else { const sf = this.getSourceFile(filePath); @@ -196,35 +202,13 @@ export class CompilerHost implements AotCompilerHost { } try { const metadataOrMetadatas = JSON.parse(this.context.readFile(filePath)); - const metadatas = metadataOrMetadatas ? + const metadatas: ModuleMetadata[] = metadataOrMetadatas ? (Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) : []; - const v1Metadata = metadatas.find((m: any) => m['version'] === 1); - let v3Metadata = metadatas.find((m: any) => m['version'] === 3); + const v1Metadata = metadatas.find(m => m.version === 1); + let v3Metadata = metadatas.find(m => m.version === 3); if (!v3Metadata && v1Metadata) { - // patch up v1 to v3 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 and the `extends` clause. - v3Metadata = {'__symbolic': 'module', 'version': 3, 'metadata': {}}; - if (v1Metadata.exports) { - v3Metadata.exports = v1Metadata.exports; - } - for (let prop in v1Metadata.metadata) { - v3Metadata.metadata[prop] = v1Metadata.metadata[prop]; - } - - const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath)); - if (exports) { - for (let prop in exports.metadata) { - if (!v3Metadata.metadata[prop]) { - v3Metadata.metadata[prop] = exports.metadata[prop]; - } - } - if (exports.exports) { - v3Metadata.exports = exports.exports; - } - } - metadatas.push(v3Metadata); + metadatas.push(this.upgradeVersion1Metadata(v1Metadata, dtsFilePath)); } this.resolverCache.set(filePath, metadatas); return metadatas; @@ -234,6 +218,32 @@ export class CompilerHost implements AotCompilerHost { } } + private upgradeVersion1Metadata(v1Metadata: ModuleMetadata, dtsFilePath: string): ModuleMetadata { + // patch up v1 to v3 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 and the `extends` clause. + let v3Metadata: ModuleMetadata = {'__symbolic': 'module', 'version': 3, 'metadata': {}}; + if (v1Metadata.exports) { + v3Metadata.exports = v1Metadata.exports; + } + for (let prop in v1Metadata.metadata) { + v3Metadata.metadata[prop] = v1Metadata.metadata[prop]; + } + + const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath)); + if (exports) { + for (let prop in exports.metadata) { + if (!v3Metadata.metadata[prop]) { + v3Metadata.metadata[prop] = exports.metadata[prop]; + } + } + if (exports.exports) { + v3Metadata.exports = exports.exports; + } + } + return v3Metadata; + } + loadResource(filePath: string): Promise { return this.context.readResource(filePath); } loadSummary(filePath: string): string|null { diff --git a/modules/@angular/compiler-cli/test/aot_host_spec.ts b/modules/@angular/compiler-cli/test/aot_host_spec.ts index 1134c9111c..2691e82cb3 100644 --- a/modules/@angular/compiler-cli/test/aot_host_spec.ts +++ b/modules/@angular/compiler-cli/test/aot_host_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ModuleMetadata} from '@angular/tsc-wrapped'; import * as ts from 'typescript'; import {CompilerHost} from '../src/compiler_host'; @@ -150,12 +151,14 @@ describe('CompilerHost', () => { it('should be able to read a metadata file', () => { expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/core.d.ts')).toEqual([ - {__symbolic: 'module', version: 2, metadata: {foo: {__symbolic: 'class'}}} + {__symbolic: 'module', version: 3, metadata: {foo: {__symbolic: 'class'}}} ]); }); it('should be able to read metadata from an otherwise unused .d.ts file ', () => { - expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/unused.d.ts')).toBeUndefined(); + expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/unused.d.ts')).toEqual([ + dummyMetadata + ]); }); it('should be able to read empty metadata ', () => { @@ -181,10 +184,21 @@ describe('CompilerHost', () => { } ]); }); + + it('should upgrade a missing metadata file into v3', () => { + expect(hostNestedGenDir.getMetadataFor('metadata_versions/v1_empty.d.ts')).toEqual([ + {__symbolic: 'module', version: 3, metadata: {}, exports: [{from: './lib/utils'}]} + ]); + }); }); const dummyModule = 'export let foo: any[];'; - +const dummyMetadata: ModuleMetadata = { + __symbolic: 'module', + version: 3, + metadata: + {foo: {__symbolic: 'error', message: 'Variable not initialized', line: 0, character: 11}} +}; const FILES: Entry = { 'tmp': { 'src': { @@ -204,7 +218,7 @@ const FILES: Entry = { '@angular': { 'core.d.ts': dummyModule, 'core.metadata.json': - `{"__symbolic":"module", "version": 2, "metadata": {"foo": {"__symbolic": "class"}}}`, + `{"__symbolic":"module", "version": 3, "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;', @@ -225,6 +239,9 @@ const FILES: Entry = { `, 'v1.metadata.json': `{"__symbolic":"module", "version": 1, "metadata": {"foo": {"__symbolic": "class"}}}`, + 'v1_empty.d.ts': ` + export * from './lib/utils'; + ` } } } diff --git a/tools/@angular/tsc-wrapped/src/collector.ts b/tools/@angular/tsc-wrapped/src/collector.ts index 543f9b583e..31a268ea90 100644 --- a/tools/@angular/tsc-wrapped/src/collector.ts +++ b/tools/@angular/tsc-wrapped/src/collector.ts @@ -224,6 +224,34 @@ export class MetadataCollector { return recordEntry(result, classDeclaration); } + // Collect all exported symbols from an exports clause. + const exportMap = new Map(); + ts.forEachChild(sourceFile, node => { + switch (node.kind) { + case ts.SyntaxKind.ExportDeclaration: + const exportDeclaration = node; + const {moduleSpecifier, exportClause} = exportDeclaration; + + if (!moduleSpecifier) { + exportClause.elements.forEach(spec => { + const exportedAs = spec.name.text; + const name = (spec.propertyName || spec.name).text; + exportMap.set(name, exportedAs); + }); + } + } + }); + + const isExportedIdentifier = (identifier: ts.Identifier) => exportMap.has(identifier.text); + const isExported = (node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration) => + (node.flags & ts.NodeFlags.Export) || isExportedIdentifier(node.name); + const exportedIdentifierName = (identifier: ts.Identifier) => + exportMap.get(identifier.text) || identifier.text; + const exportedName = + (node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration) => + exportedIdentifierName(node.name); + + // Predeclare classes and functions ts.forEachChild(sourceFile, node => { switch (node.kind) { @@ -231,8 +259,9 @@ export class MetadataCollector { const classDeclaration = node; if (classDeclaration.name) { const className = classDeclaration.name.text; - if (node.flags & ts.NodeFlags.Export) { - locals.define(className, {__symbolic: 'reference', name: className}); + if (isExported(classDeclaration)) { + locals.define( + className, {__symbolic: 'reference', name: exportedName(classDeclaration)}); } else { locals.define( className, errorSym('Reference to non-exported class', node, {className})); @@ -241,9 +270,9 @@ export class MetadataCollector { break; case ts.SyntaxKind.FunctionDeclaration: - if (!(node.flags & ts.NodeFlags.Export)) { + const functionDeclaration = node; + if (!isExported(functionDeclaration)) { // Report references to this function as an error. - const functionDeclaration = node; const nameNode = functionDeclaration.name; if (nameNode && nameNode.text) { locals.define( @@ -268,10 +297,14 @@ export class MetadataCollector { if (exportClause) { exportClause.elements.forEach(spec => { const name = spec.name.text; - const propNode = spec.propertyName || spec.name; - const value: MetadataValue = evaluator.evaluateNode(propNode); - if (!metadata) metadata = {}; - metadata[name] = recordEntry(value, node); + // If the symbol was not already exported, export a reference since it is a + // reference to an import + if (!metadata || !metadata[name]) { + const propNode = spec.propertyName || spec.name; + const value: MetadataValue = evaluator.evaluateNode(propNode); + if (!metadata) metadata = {}; + metadata[name] = recordEntry(value, node); + } }); } } @@ -294,9 +327,9 @@ export class MetadataCollector { const classDeclaration = node; if (classDeclaration.name) { const className = classDeclaration.name.text; - if (node.flags & ts.NodeFlags.Export) { + if (isExported(classDeclaration)) { if (!metadata) metadata = {}; - metadata[className] = classMetadataOf(classDeclaration); + metadata[exportedName(classDeclaration)] = classMetadataOf(classDeclaration); } } // Otherwise don't record metadata for the class. @@ -306,24 +339,20 @@ export class MetadataCollector { // Record functions that return a single value. Record the parameter // names substitution will be performed by the StaticReflector. const functionDeclaration = node; - if (node.flags & ts.NodeFlags.Export) { + if (isExported(functionDeclaration)) { if (!metadata) metadata = {}; + const name = exportedName(functionDeclaration); const maybeFunc = maybeGetSimpleFunction(functionDeclaration); - if (maybeFunc) { - metadata[maybeFunc.name] = recordEntry(maybeFunc.func, node); - } else if (functionDeclaration.name.kind == ts.SyntaxKind.Identifier) { - const nameNode = functionDeclaration.name; - const functionName = nameNode.text; - metadata[functionName] = {__symbolic: 'function'}; - } + metadata[name] = + maybeFunc ? recordEntry(maybeFunc.func, node) : {__symbolic: 'function'}; } break; case ts.SyntaxKind.EnumDeclaration: - if (node.flags & ts.NodeFlags.Export) { - const enumDeclaration = node; + const enumDeclaration = node; + if (isExported(enumDeclaration)) { const enumValueHolder: {[name: string]: MetadataValue} = {}; - const enumName = enumDeclaration.name.text; + const enumName = exportedName(enumDeclaration); let nextDefaultValue: MetadataValue = 0; let writtenMembers = 0; for (const member of enumDeclaration.members) { @@ -376,9 +405,10 @@ export class MetadataCollector { } let exported = false; if (variableStatement.flags & ts.NodeFlags.Export || - variableDeclaration.flags & ts.NodeFlags.Export) { + variableDeclaration.flags & ts.NodeFlags.Export || + isExportedIdentifier(nameNode)) { if (!metadata) metadata = {}; - metadata[nameNode.text] = recordEntry(varValue, node); + metadata[exportedIdentifierName(nameNode)] = recordEntry(varValue, node); exported = true; } if (isPrimitive(varValue)) { diff --git a/tools/@angular/tsc-wrapped/test/collector.spec.ts b/tools/@angular/tsc-wrapped/test/collector.spec.ts index b020579026..8ad1e39c8d 100644 --- a/tools/@angular/tsc-wrapped/test/collector.spec.ts +++ b/tools/@angular/tsc-wrapped/test/collector.spec.ts @@ -40,6 +40,7 @@ describe('Collector', () => { 'private-enum.ts', 're-exports.ts', 're-exports-2.ts', + 'export-as.d.ts', 'static-field-reference.ts', 'static-method.ts', 'static-method-call.ts', @@ -528,20 +529,19 @@ describe('Collector', () => { ]); }); + it('should be able to collect a export as symbol', () => { + const source = program.getSourceFile('export-as.d.ts'); + const metadata = collector.getMetadata(source); + expect(metadata.metadata).toEqual({SomeFunction: {__symbolic: 'function'}}); + }); + it('should be able to collect exports with no module specifier', () => { const source = program.getSourceFile('/re-exports-2.ts'); const metadata = collector.getMetadata(source); expect(metadata.metadata).toEqual({ + MyClass: Object({__symbolic: 'class'}), OtherModule: {__symbolic: 'reference', module: './static-field-reference', name: 'Foo'}, - MyOtherModule: {__symbolic: 'reference', module: './static-field', name: 'MyModule'}, - // TODO(vicb): support exported symbols - https://github.com/angular/angular/issues/13473 - MyClass: { - __symbolic: 'error', - message: 'Reference to non-exported class', - line: 3, - character: 4, - context: {className: 'MyClass'} - }, + MyOtherModule: {__symbolic: 'reference', module: './static-field', name: 'MyModule'} }); }); @@ -1007,6 +1007,10 @@ const FILES: Directory = { class MyClass {} export {OtherModule, MyModule as MyOtherModule, MyClass}; `, + 'export-as.d.ts': ` + declare function someFunction(): void; + export { someFunction as SomeFunction }; + `, 'local-symbol-ref.ts': ` import {Component, Validators} from 'angular2/core';