fix(compiler-cli): produce metadata for .d.ts files without metadata (#13526)
Fixes #13307 Fixes #13473 Fixes #13521
This commit is contained in:
parent
e78508507d
commit
0e3981afc1
|
@ -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<string> { return this.context.readResource(filePath); }
|
||||
|
||||
loadSummary(filePath: string): string|null {
|
||||
|
|
|
@ -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';
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -224,6 +224,34 @@ export class MetadataCollector {
|
|||
return recordEntry(result, classDeclaration);
|
||||
}
|
||||
|
||||
// Collect all exported symbols from an exports clause.
|
||||
const exportMap = new Map<string, string>();
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ExportDeclaration:
|
||||
const exportDeclaration = <ts.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 = <ts.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 = <ts.FunctionDeclaration>node;
|
||||
if (!isExported(functionDeclaration)) {
|
||||
// Report references to this function as an error.
|
||||
const functionDeclaration = <ts.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 = <ts.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 = <ts.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 = <ts.Identifier>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 = <ts.EnumDeclaration>node;
|
||||
const enumDeclaration = <ts.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)) {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Reference in New Issue