fix(compiler-cli): produce metadata for .d.ts files without metadata (#13526)
Fixes #13307 Fixes #13473 Fixes #13521
This commit is contained in:
@ -181,6 +181,12 @@ export class CompilerHost implements AotCompilerHost {
|
|||||||
const metadataPath = filePath.replace(DTS, '.metadata.json');
|
const metadataPath = filePath.replace(DTS, '.metadata.json');
|
||||||
if (this.context.fileExists(metadataPath)) {
|
if (this.context.fileExists(metadataPath)) {
|
||||||
return this.readMetadata(metadataPath, filePath);
|
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 {
|
} else {
|
||||||
const sf = this.getSourceFile(filePath);
|
const sf = this.getSourceFile(filePath);
|
||||||
@ -196,35 +202,13 @@ export class CompilerHost implements AotCompilerHost {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const metadataOrMetadatas = JSON.parse(this.context.readFile(filePath));
|
const metadataOrMetadatas = JSON.parse(this.context.readFile(filePath));
|
||||||
const metadatas = metadataOrMetadatas ?
|
const metadatas: ModuleMetadata[] = metadataOrMetadatas ?
|
||||||
(Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) :
|
(Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) :
|
||||||
[];
|
[];
|
||||||
const v1Metadata = metadatas.find((m: any) => m['version'] === 1);
|
const v1Metadata = metadatas.find(m => m.version === 1);
|
||||||
let v3Metadata = metadatas.find((m: any) => m['version'] === 3);
|
let v3Metadata = metadatas.find(m => m.version === 3);
|
||||||
if (!v3Metadata && v1Metadata) {
|
if (!v3Metadata && v1Metadata) {
|
||||||
// patch up v1 to v3 by merging the metadata with metadata collected from the d.ts file
|
metadatas.push(this.upgradeVersion1Metadata(v1Metadata, dtsFilePath));
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
this.resolverCache.set(filePath, metadatas);
|
this.resolverCache.set(filePath, metadatas);
|
||||||
return 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); }
|
loadResource(filePath: string): Promise<string> { return this.context.readResource(filePath); }
|
||||||
|
|
||||||
loadSummary(filePath: string): string|null {
|
loadSummary(filePath: string): string|null {
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {ModuleMetadata} from '@angular/tsc-wrapped';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {CompilerHost} from '../src/compiler_host';
|
import {CompilerHost} from '../src/compiler_host';
|
||||||
@ -150,12 +151,14 @@ describe('CompilerHost', () => {
|
|||||||
|
|
||||||
it('should be able to read a metadata file', () => {
|
it('should be able to read a metadata file', () => {
|
||||||
expect(hostNestedGenDir.getMetadataFor('node_modules/@angular/core.d.ts')).toEqual([
|
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 ', () => {
|
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 ', () => {
|
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 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 = {
|
const FILES: Entry = {
|
||||||
'tmp': {
|
'tmp': {
|
||||||
'src': {
|
'src': {
|
||||||
@ -204,7 +218,7 @@ const FILES: Entry = {
|
|||||||
'@angular': {
|
'@angular': {
|
||||||
'core.d.ts': dummyModule,
|
'core.d.ts': dummyModule,
|
||||||
'core.metadata.json':
|
'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}},
|
'router': {'index.d.ts': dummyModule, 'src': {'providers.d.ts': dummyModule}},
|
||||||
'unused.d.ts': dummyModule,
|
'unused.d.ts': dummyModule,
|
||||||
'empty.d.ts': 'export declare var a: string;',
|
'empty.d.ts': 'export declare var a: string;',
|
||||||
@ -225,6 +239,9 @@ const FILES: Entry = {
|
|||||||
`,
|
`,
|
||||||
'v1.metadata.json':
|
'v1.metadata.json':
|
||||||
`{"__symbolic":"module", "version": 1, "metadata": {"foo": {"__symbolic": "class"}}}`,
|
`{"__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);
|
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
|
// Predeclare classes and functions
|
||||||
ts.forEachChild(sourceFile, node => {
|
ts.forEachChild(sourceFile, node => {
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
@ -231,8 +259,9 @@ export class MetadataCollector {
|
|||||||
const classDeclaration = <ts.ClassDeclaration>node;
|
const classDeclaration = <ts.ClassDeclaration>node;
|
||||||
if (classDeclaration.name) {
|
if (classDeclaration.name) {
|
||||||
const className = classDeclaration.name.text;
|
const className = classDeclaration.name.text;
|
||||||
if (node.flags & ts.NodeFlags.Export) {
|
if (isExported(classDeclaration)) {
|
||||||
locals.define(className, {__symbolic: 'reference', name: className});
|
locals.define(
|
||||||
|
className, {__symbolic: 'reference', name: exportedName(classDeclaration)});
|
||||||
} else {
|
} else {
|
||||||
locals.define(
|
locals.define(
|
||||||
className, errorSym('Reference to non-exported class', node, {className}));
|
className, errorSym('Reference to non-exported class', node, {className}));
|
||||||
@ -241,9 +270,9 @@ export class MetadataCollector {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ts.SyntaxKind.FunctionDeclaration:
|
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.
|
// Report references to this function as an error.
|
||||||
const functionDeclaration = <ts.FunctionDeclaration>node;
|
|
||||||
const nameNode = functionDeclaration.name;
|
const nameNode = functionDeclaration.name;
|
||||||
if (nameNode && nameNode.text) {
|
if (nameNode && nameNode.text) {
|
||||||
locals.define(
|
locals.define(
|
||||||
@ -268,10 +297,14 @@ export class MetadataCollector {
|
|||||||
if (exportClause) {
|
if (exportClause) {
|
||||||
exportClause.elements.forEach(spec => {
|
exportClause.elements.forEach(spec => {
|
||||||
const name = spec.name.text;
|
const name = spec.name.text;
|
||||||
const propNode = spec.propertyName || spec.name;
|
// If the symbol was not already exported, export a reference since it is a
|
||||||
const value: MetadataValue = evaluator.evaluateNode(propNode);
|
// reference to an import
|
||||||
if (!metadata) metadata = {};
|
if (!metadata || !metadata[name]) {
|
||||||
metadata[name] = recordEntry(value, node);
|
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;
|
const classDeclaration = <ts.ClassDeclaration>node;
|
||||||
if (classDeclaration.name) {
|
if (classDeclaration.name) {
|
||||||
const className = classDeclaration.name.text;
|
const className = classDeclaration.name.text;
|
||||||
if (node.flags & ts.NodeFlags.Export) {
|
if (isExported(classDeclaration)) {
|
||||||
if (!metadata) metadata = {};
|
if (!metadata) metadata = {};
|
||||||
metadata[className] = classMetadataOf(classDeclaration);
|
metadata[exportedName(classDeclaration)] = classMetadataOf(classDeclaration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise don't record metadata for the class.
|
// 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
|
// Record functions that return a single value. Record the parameter
|
||||||
// names substitution will be performed by the StaticReflector.
|
// names substitution will be performed by the StaticReflector.
|
||||||
const functionDeclaration = <ts.FunctionDeclaration>node;
|
const functionDeclaration = <ts.FunctionDeclaration>node;
|
||||||
if (node.flags & ts.NodeFlags.Export) {
|
if (isExported(functionDeclaration)) {
|
||||||
if (!metadata) metadata = {};
|
if (!metadata) metadata = {};
|
||||||
|
const name = exportedName(functionDeclaration);
|
||||||
const maybeFunc = maybeGetSimpleFunction(functionDeclaration);
|
const maybeFunc = maybeGetSimpleFunction(functionDeclaration);
|
||||||
if (maybeFunc) {
|
metadata[name] =
|
||||||
metadata[maybeFunc.name] = recordEntry(maybeFunc.func, node);
|
maybeFunc ? recordEntry(maybeFunc.func, node) : {__symbolic: 'function'};
|
||||||
} else if (functionDeclaration.name.kind == ts.SyntaxKind.Identifier) {
|
|
||||||
const nameNode = <ts.Identifier>functionDeclaration.name;
|
|
||||||
const functionName = nameNode.text;
|
|
||||||
metadata[functionName] = {__symbolic: 'function'};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ts.SyntaxKind.EnumDeclaration:
|
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 enumValueHolder: {[name: string]: MetadataValue} = {};
|
||||||
const enumName = enumDeclaration.name.text;
|
const enumName = exportedName(enumDeclaration);
|
||||||
let nextDefaultValue: MetadataValue = 0;
|
let nextDefaultValue: MetadataValue = 0;
|
||||||
let writtenMembers = 0;
|
let writtenMembers = 0;
|
||||||
for (const member of enumDeclaration.members) {
|
for (const member of enumDeclaration.members) {
|
||||||
@ -376,9 +405,10 @@ export class MetadataCollector {
|
|||||||
}
|
}
|
||||||
let exported = false;
|
let exported = false;
|
||||||
if (variableStatement.flags & ts.NodeFlags.Export ||
|
if (variableStatement.flags & ts.NodeFlags.Export ||
|
||||||
variableDeclaration.flags & ts.NodeFlags.Export) {
|
variableDeclaration.flags & ts.NodeFlags.Export ||
|
||||||
|
isExportedIdentifier(nameNode)) {
|
||||||
if (!metadata) metadata = {};
|
if (!metadata) metadata = {};
|
||||||
metadata[nameNode.text] = recordEntry(varValue, node);
|
metadata[exportedIdentifierName(nameNode)] = recordEntry(varValue, node);
|
||||||
exported = true;
|
exported = true;
|
||||||
}
|
}
|
||||||
if (isPrimitive(varValue)) {
|
if (isPrimitive(varValue)) {
|
||||||
|
@ -40,6 +40,7 @@ describe('Collector', () => {
|
|||||||
'private-enum.ts',
|
'private-enum.ts',
|
||||||
're-exports.ts',
|
're-exports.ts',
|
||||||
're-exports-2.ts',
|
're-exports-2.ts',
|
||||||
|
'export-as.d.ts',
|
||||||
'static-field-reference.ts',
|
'static-field-reference.ts',
|
||||||
'static-method.ts',
|
'static-method.ts',
|
||||||
'static-method-call.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', () => {
|
it('should be able to collect exports with no module specifier', () => {
|
||||||
const source = program.getSourceFile('/re-exports-2.ts');
|
const source = program.getSourceFile('/re-exports-2.ts');
|
||||||
const metadata = collector.getMetadata(source);
|
const metadata = collector.getMetadata(source);
|
||||||
expect(metadata.metadata).toEqual({
|
expect(metadata.metadata).toEqual({
|
||||||
|
MyClass: Object({__symbolic: 'class'}),
|
||||||
OtherModule: {__symbolic: 'reference', module: './static-field-reference', name: 'Foo'},
|
OtherModule: {__symbolic: 'reference', module: './static-field-reference', name: 'Foo'},
|
||||||
MyOtherModule: {__symbolic: 'reference', module: './static-field', name: 'MyModule'},
|
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'}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1007,6 +1007,10 @@ const FILES: Directory = {
|
|||||||
class MyClass {}
|
class MyClass {}
|
||||||
export {OtherModule, MyModule as MyOtherModule, MyClass};
|
export {OtherModule, MyModule as MyOtherModule, MyClass};
|
||||||
`,
|
`,
|
||||||
|
'export-as.d.ts': `
|
||||||
|
declare function someFunction(): void;
|
||||||
|
export { someFunction as SomeFunction };
|
||||||
|
`,
|
||||||
'local-symbol-ref.ts': `
|
'local-symbol-ref.ts': `
|
||||||
import {Component, Validators} from 'angular2/core';
|
import {Component, Validators} from 'angular2/core';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user