Skip to content

Commit 82e7ecd

Browse files
authored
fix(compiler): StaticReflect now resolves re-exported symbols (angular#10453)
Fixes: angular#10451
1 parent 3d53b33 commit 82e7ecd

File tree

5 files changed

+208
-14
lines changed

5 files changed

+208
-14
lines changed

modules/@angular/compiler-cli/src/reflector_host.ts

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
4343
provider: '@angular/core/src/di/provider'
4444
};
4545
}
46+
4647
private resolve(m: string, containingFile: string) {
4748
const resolved =
4849
ts.resolveModuleName(m, containingFile, this.options, this.context).resolvedModule;
@@ -72,12 +73,9 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
7273
importedFile = this.resolveAssetUrl(importedFile, containingFile);
7374
containingFile = this.resolveAssetUrl(containingFile, '');
7475

75-
// TODO(tbosch): if a file does not yet exist (because we compile it later),
76-
// we still need to create it so that the `resolve` method works!
76+
// If a file does not yet exist (because we compile it later), we still need to
77+
// assume it exists it so that the `resolve` method works!
7778
if (!this.compilerHost.fileExists(importedFile)) {
78-
if (this.options.trace) {
79-
console.log(`Generating empty file ${importedFile} to allow resolution of import`);
80-
}
8179
this.context.assumeFileExists(importedFile);
8280
}
8381

@@ -133,11 +131,10 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
133131
const sf = this.program.getSourceFile(filePath);
134132
if (!sf || !(<any>sf).symbol) {
135133
// The source file was not needed in the compile but we do need the values from
136-
// the corresponding .ts files stored in the .metadata.json file. Just assume the
137-
// symbol and file we resolved to be correct as we don't need this to be the
138-
// cannonical reference as this reference could have only been generated by a
139-
// .metadata.json file resolving values.
140-
return this.getStaticSymbol(filePath, symbolName);
134+
// the corresponding .ts files stored in the .metadata.json file. Check the file
135+
// for exports to see if the file is exported.
136+
return this.resolveExportedSymbol(filePath, symbolName) ||
137+
this.getStaticSymbol(filePath, symbolName);
141138
}
142139

143140
let symbol = tc.getExportsOfModule((<any>sf).symbol).find(m => m.name === symbolName);
@@ -159,6 +156,7 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
159156
}
160157

161158
private typeCache = new Map<string, StaticSymbol>();
159+
private resolverCache = new Map<string, ModuleMetadata>();
162160

163161
/**
164162
* getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded.
@@ -200,13 +198,71 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
200198

201199
readMetadata(filePath: string) {
202200
try {
203-
const result = JSON.parse(this.context.readFile(filePath));
204-
return result;
201+
return this.resolverCache.get(filePath) || JSON.parse(this.context.readFile(filePath));
205202
} catch (e) {
206203
console.error(`Failed to read JSON file ${filePath}`);
207204
throw e;
208205
}
209206
}
207+
208+
private getResolverMetadata(filePath: string): ModuleMetadata {
209+
let metadata = this.resolverCache.get(filePath);
210+
if (!metadata) {
211+
metadata = this.getMetadataFor(filePath);
212+
this.resolverCache.set(filePath, metadata);
213+
}
214+
return metadata;
215+
}
216+
217+
private resolveExportedSymbol(filePath: string, symbolName: string): StaticSymbol {
218+
const resolveModule = (moduleName: string): string => {
219+
const resolvedModulePath = this.resolve(moduleName, filePath);
220+
if (!resolvedModulePath) {
221+
throw new Error(`Could not resolve module '${moduleName}' relative to file ${filePath}`);
222+
}
223+
return resolvedModulePath;
224+
};
225+
let metadata = this.getResolverMetadata(filePath);
226+
if (metadata) {
227+
// If we have metadata for the symbol, this is the original exporting location.
228+
if (metadata.metadata[symbolName]) {
229+
return this.getStaticSymbol(filePath, symbolName);
230+
}
231+
232+
// If no, try to find the symbol in one of the re-export location
233+
if (metadata.exports) {
234+
// Try and find the symbol in the list of explicitly re-exported symbols.
235+
for (const moduleExport of metadata.exports) {
236+
if (moduleExport.export) {
237+
const exportSymbol = moduleExport.export.find(symbol => {
238+
if (typeof symbol === 'string') {
239+
return symbol == symbolName;
240+
} else {
241+
return symbol.as == symbolName;
242+
}
243+
});
244+
if (exportSymbol) {
245+
let symName = symbolName;
246+
if (typeof exportSymbol !== 'string') {
247+
symName = exportSymbol.name;
248+
}
249+
return this.resolveExportedSymbol(resolveModule(moduleExport.from), symName);
250+
}
251+
}
252+
}
253+
254+
// Try to find the symbol via export * directives.
255+
for (const moduleExport of metadata.exports) {
256+
if (!moduleExport.export) {
257+
const resolvedModule = resolveModule(moduleExport.from);
258+
const candidateSymbol = this.resolveExportedSymbol(resolvedModule, symbolName);
259+
if (candidateSymbol) return candidateSymbol;
260+
}
261+
}
262+
}
263+
}
264+
return null;
265+
}
210266
}
211267

212268
export class NodeReflectorHostContext implements ReflectorHostContext {

modules/@angular/compiler-cli/test/reflector_host_spec.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,34 @@ describe('reflector_host', () => {
105105
it('should return undefined for missing modules', () => {
106106
expect(reflectorHost.getMetadataFor('node_modules/@angular/missing.d.ts')).toBeUndefined();
107107
});
108+
109+
it('should be able to trace a named export', () => {
110+
const symbol =
111+
reflectorHost.findDeclaration('./reexport/reexport.d.ts', 'One', '/tmp/src/main.ts');
112+
expect(symbol.name).toEqual('One');
113+
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
114+
});
115+
116+
it('should be able to trace a renamed export', () => {
117+
const symbol =
118+
reflectorHost.findDeclaration('./reexport/reexport.d.ts', 'Four', '/tmp/src/main.ts');
119+
expect(symbol.name).toEqual('Three');
120+
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
121+
});
122+
123+
it('should be able to trace an export * export', () => {
124+
const symbol =
125+
reflectorHost.findDeclaration('./reexport/reexport.d.ts', 'Five', '/tmp/src/main.ts');
126+
expect(symbol.name).toEqual('Five');
127+
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin5.d.ts');
128+
});
129+
130+
it('should be able to trace a multi-level re-export', () => {
131+
const symbol =
132+
reflectorHost.findDeclaration('./reexport/reexport.d.ts', 'Thirty', '/tmp/src/main.ts');
133+
expect(symbol.name).toEqual('Thirty');
134+
expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin30.d.ts');
135+
});
108136
});
109137

110138
const dummyModule = 'export let foo: any[];';
@@ -124,6 +152,69 @@ const FILES: Entry = {
124152
'collections.ts': dummyModule,
125153
},
126154
'lib2': {'utils2.ts': dummyModule},
155+
'reexport': {
156+
'reexport.d.ts': `
157+
import * as c from '@angular/core';
158+
`,
159+
'reexport.metadata.json': JSON.stringify({
160+
__symbolic: 'module',
161+
version: 1,
162+
metadata: {},
163+
exports: [
164+
{from: './src/origin1', export: ['One', 'Two', {name: 'Three', as: 'Four'}]},
165+
{from: './src/origin5'}, {from: './src/reexport2'}
166+
]
167+
}),
168+
'src': {
169+
'origin1.d.ts': `
170+
export class One {}
171+
export class Two {}
172+
export class Three {}
173+
`,
174+
'origin1.metadata.json': JSON.stringify({
175+
__symbolic: 'module',
176+
version: 1,
177+
metadata: {
178+
One: {__symbolic: 'class'},
179+
Two: {__symbolic: 'class'},
180+
Three: {__symbolic: 'class'},
181+
},
182+
}),
183+
'origin5.d.ts': `
184+
export class Five {}
185+
`,
186+
'origin5.metadata.json': JSON.stringify({
187+
__symbolic: 'module',
188+
version: 1,
189+
metadata: {
190+
Five: {__symbolic: 'class'},
191+
},
192+
}),
193+
'origin30.d.ts': `
194+
export class Thirty {}
195+
`,
196+
'origin30.metadata.json': JSON.stringify({
197+
__symbolic: 'module',
198+
version: 1,
199+
metadata: {
200+
Thirty: {__symbolic: 'class'},
201+
},
202+
}),
203+
'originNone.d.ts': dummyModule,
204+
'originNone.metadata.json': JSON.stringify({
205+
__symbolic: 'module',
206+
version: 1,
207+
metadata: {},
208+
}),
209+
'reexport2.d.ts': dummyModule,
210+
'reexport2.metadata.json': JSON.stringify({
211+
__symbolic: 'module',
212+
version: 1,
213+
metadata: {},
214+
exports: [{from: './originNone'}, {from: './origin30'}]
215+
})
216+
}
217+
},
127218
'node_modules': {
128219
'@angular': {
129220
'core.d.ts': dummyModule,

tools/@angular/tsc-wrapped/src/collector.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as ts from 'typescript';
22

33
import {Evaluator, errorSymbol, isPrimitive} from './evaluator';
4-
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isMetadataError, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression} from './schema';
4+
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isMetadataError, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression} from './schema';
55
import {Symbols} from './symbols';
66

77

@@ -20,6 +20,7 @@ export class MetadataCollector {
2020
const locals = new Symbols(sourceFile);
2121
const evaluator = new Evaluator(locals);
2222
let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined;
23+
let exports: ModuleExportMetadata[];
2324

2425
function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression {
2526
return <MetadataSymbolicExpression>evaluator.evaluateNode(decoratorNode.expression);
@@ -202,6 +203,25 @@ export class MetadataCollector {
202203
});
203204
ts.forEachChild(sourceFile, node => {
204205
switch (node.kind) {
206+
case ts.SyntaxKind.ExportDeclaration:
207+
// Record export declarations
208+
const exportDeclaration = <ts.ExportDeclaration>node;
209+
const moduleSpecifier = exportDeclaration.moduleSpecifier;
210+
if (moduleSpecifier && moduleSpecifier.kind == ts.SyntaxKind.StringLiteral) {
211+
// Ignore exports that don't have string literals as exports.
212+
// This is allowed by the syntax but will be flagged as an error by the type checker.
213+
const from = (<ts.StringLiteral>moduleSpecifier).text;
214+
const moduleExport: ModuleExportMetadata = {from};
215+
if (exportDeclaration.exportClause) {
216+
moduleExport.export = exportDeclaration.exportClause.elements.map(
217+
element => element.propertyName ?
218+
{name: element.propertyName.text, as: element.name.text} :
219+
element.name.text)
220+
}
221+
if (!exports) exports = [];
222+
exports.push(moduleExport);
223+
}
224+
break;
205225
case ts.SyntaxKind.ClassDeclaration:
206226
const classDeclaration = <ts.ClassDeclaration>node;
207227
const className = classDeclaration.name.text;
@@ -320,7 +340,12 @@ export class MetadataCollector {
320340
}
321341
});
322342

323-
return metadata && {__symbolic: 'module', version: VERSION, metadata};
343+
if (metadata || exports) {
344+
if (!metadata) metadata = {};
345+
const result: ModuleMetadata = {__symbolic: 'module', version: VERSION, metadata};
346+
if (exports) result.exports = exports;
347+
return result;
348+
}
324349
}
325350
}
326351

tools/@angular/tsc-wrapped/src/schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@ export const VERSION = 1;
1212
export interface ModuleMetadata {
1313
__symbolic: 'module';
1414
version: number;
15+
exports?: ModuleExportMetadata[];
1516
metadata: {[name: string]: (ClassMetadata | FunctionMetadata | MetadataValue)};
1617
}
1718
export function isModuleMetadata(value: any): value is ModuleMetadata {
1819
return value && value.__symbolic === 'module';
1920
}
2021

22+
export interface ModuleExportMetadata {
23+
export?: (string|{name: string, as: string})[];
24+
from: string;
25+
}
26+
2127
export interface ClassMetadata {
2228
__symbolic: 'class';
2329
decorators?: (MetadataSymbolicExpression|MetadataError)[];

tools/@angular/tsc-wrapped/test/collector.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('Collector', () => {
2424
'exported-functions.ts',
2525
'exported-enum.ts',
2626
'exported-consts.ts',
27+
're-exports.ts',
2728
'static-field-reference.ts',
2829
'static-method.ts',
2930
'static-method-call.ts',
@@ -475,6 +476,16 @@ describe('Collector', () => {
475476
}
476477
});
477478
});
479+
480+
it('should be able to collect re-exported symbols', () => {
481+
let source = program.getSourceFile('/re-exports.ts');
482+
let metadata = collector.getMetadata(source);
483+
expect(metadata.exports).toEqual([
484+
{from: './static-field', export: ['MyModule']},
485+
{from: './static-field-reference.ts', export: [{name: 'Foo', as: 'OtherModule'}]},
486+
{from: 'angular2/core'}
487+
]);
488+
});
478489
});
479490

480491
// TODO: Do not use \` in a template literal as it confuses clang-format
@@ -783,6 +794,11 @@ const FILES: Directory = {
783794
}
784795
}
785796
`,
797+
're-exports.ts': `
798+
export {MyModule} from './static-field';
799+
export {Foo as OtherModule} from './static-field-reference.ts';
800+
export * from 'angular2/core';
801+
`,
786802
'node_modules': {
787803
'angular2': {
788804
'core.d.ts': `

0 commit comments

Comments
 (0)