fix(compiler): look for flat module resources using declaration module path (#15367)

`ngc` would look for flat module resources relative to the flat module index.
`ngc` now looks for flat module resources relative to the `.d.ts` file that declarates
the component.

Fixes #15221

PR Close #15367
This commit is contained in:
Chuck Jazdzewski
2017-03-20 16:31:11 -07:00
committed by Miško Hevery
parent 7354949763
commit 90d2518d9a
19 changed files with 311 additions and 17 deletions

View File

@ -42,6 +42,7 @@ export class StaticAndDynamicReflectionCapabilities {
setter(name: string): ɵSetterFn { return this.dynamicDelegate.setter(name); }
method(name: string): ɵMethodFn { return this.dynamicDelegate.method(name); }
importUri(type: any): string { return this.staticDelegate.importUri(type); }
resourceUri(type: any): string { return this.staticDelegate.resourceUri(type); }
resolveIdentifier(name: string, moduleUrl: string, members: string[], runtime: any) {
return this.staticDelegate.resolveIdentifier(name, moduleUrl, members);
}

View File

@ -54,6 +54,11 @@ export class StaticReflector implements ɵReflectorReader {
return staticSymbol ? staticSymbol.filePath : null;
}
resourceUri(typeOrFunc: StaticSymbol): string {
const staticSymbol = this.findSymbolDeclaration(typeOrFunc);
return this.symbolResolver.getResourcePath(staticSymbol);
}
resolveIdentifier(name: string, moduleUrl: string, members: string[]): StaticSymbol {
const importSymbol = this.getStaticSymbol(moduleUrl, name);
const rootSymbol = this.findDeclaration(moduleUrl, name);

View File

@ -58,6 +58,7 @@ export class StaticSymbolResolver {
private resolvedFilePaths = new Set<string>();
// Note: this will only contain StaticSymbols without members!
private importAs = new Map<StaticSymbol, StaticSymbol>();
private symbolResourcePaths = new Map<StaticSymbol, string>();
constructor(
private host: StaticSymbolResolverHost, private staticSymbolCache: StaticSymbolCache,
@ -108,6 +109,15 @@ export class StaticSymbolResolver {
return result;
}
/**
* getResourcePath produces the path to the original location of the symbol and should
* be used to determine the relative location of resource references recorded in
* symbol metadata.
*/
getResourcePath(staticSymbol: StaticSymbol): string {
return this.symbolResourcePaths.get(staticSymbol) || staticSymbol.filePath;
}
/**
* getTypeArity returns the number of generic type parameters the given symbol
* has. If the symbol is not a type the result is null.
@ -200,18 +210,35 @@ export class StaticSymbolResolver {
// handle direct declarations of the symbol
const topLevelSymbolNames =
new Set<string>(Object.keys(metadata['metadata']).map(unescapeIdentifier));
const origins: {[index: string]: string} = metadata['origins'] || {};
Object.keys(metadata['metadata']).forEach((metadataKey) => {
const symbolMeta = metadata['metadata'][metadataKey];
const name = unescapeIdentifier(metadataKey);
const canonicalSymbol = this.getStaticSymbol(filePath, name);
const symbol = this.getStaticSymbol(filePath, name);
let importSymbol: StaticSymbol|undefined = undefined;
if (metadata['importAs']) {
// Index bundle indexes should use the importAs module name instead of a reference
// to the .d.ts file directly.
const importSymbol = this.getStaticSymbol(metadata['importAs'], name);
this.recordImportAs(canonicalSymbol, importSymbol);
importSymbol = this.getStaticSymbol(metadata['importAs'], name);
this.recordImportAs(symbol, importSymbol);
}
const origin = origins[metadataKey];
if (origin) {
// If the symbol is from a bundled index, use the declaration location of the
// symbol so relative references (such as './my.html') will be calculated
// correctly.
const originFilePath = this.resolveModule(origin, filePath);
if (!originFilePath) {
this.reportError(
new Error(`Couldn't resolve original symbol for ${origin} from ${filePath}`), null);
} else {
this.symbolResourcePaths.set(symbol, originFilePath);
}
}
resolvedSymbols.push(
this.createResolvedSymbol(canonicalSymbol, topLevelSymbolNames, symbolMeta));
this.createResolvedSymbol(symbol, filePath, topLevelSymbolNames, symbolMeta));
});
}
@ -257,7 +284,7 @@ export class StaticSymbolResolver {
}
private createResolvedSymbol(
sourceSymbol: StaticSymbol, topLevelSymbolNames: Set<string>,
sourceSymbol: StaticSymbol, topLevelPath: string, topLevelSymbolNames: Set<string>,
metadata: any): ResolvedStaticSymbol {
const self = this;
@ -291,7 +318,7 @@ export class StaticSymbolResolver {
return {__symbolic: 'reference', name: name};
} else {
if (topLevelSymbolNames.has(name)) {
return self.getStaticSymbol(sourceSymbol.filePath, name);
return self.getStaticSymbol(topLevelPath, name);
}
// ambient value
null;

View File

@ -1060,7 +1060,7 @@ function isValidType(value: any): boolean {
export function componentModuleUrl(
reflector: ɵReflectorReader, type: Type<any>, cmpMetadata: Component): string {
if (type instanceof StaticSymbol) {
return type.filePath;
return reflector.resourceUri(type);
}
const moduleId = cmpMetadata.moduleId;

View File

@ -415,6 +415,49 @@ describe('compiler (bundled Angular)', () => {
.toBeDefined();
})));
});
describe('Bundled libary', () => {
let host: MockCompilerHost;
let aotHost: MockAotCompilerHost;
let libraryFiles: Map<string, string>;
beforeAll(() => {
// Emit the library bundle
const emittingHost =
new EmittingCompilerHost(['/bolder/index.ts'], {emitMetadata: false, mockData: LIBRARY});
// Create the metadata bundled
const indexModule = '/bolder/public-api';
const bundler =
new MetadataBundler(indexModule, 'bolder', new MockMetadataBundlerHost(emittingHost));
const bundle = bundler.getMetadataBundle();
const metadata = JSON.stringify(bundle.metadata, null, ' ');
const bundleIndexSource = privateEntriesToIndex('./public-api', bundle.privates);
emittingHost.override('/bolder/index.ts', bundleIndexSource);
emittingHost.addWrittenFile('/bolder/index.metadata.json', metadata);
// Emit the sources
const emittingProgram = ts.createProgram(['/bolder/index.ts'], settings, emittingHost);
emittingProgram.emit();
libraryFiles = emittingHost.written;
// Copy the .html file
const htmlFileName = '/bolder/src/bolder.component.html';
libraryFiles.set(htmlFileName, emittingHost.readFile(htmlFileName));
});
beforeEach(() => {
host = new MockCompilerHost(
LIBRARY_USING_APP_MODULE, LIBRARY_USING_APP, angularFiles, [libraryFiles]);
aotHost = new MockAotCompilerHost(host);
});
it('should compile',
async(() => compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics)));
// Restore reflector since AoT compiler will update it with a new static reflector
afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); });
});
});
function expectNoDiagnostics(program: ts.Program) {
@ -547,6 +590,72 @@ const FILES: MockData = {
}
};
const LIBRARY: MockData = {
bolder: {
'public-api.ts': `
export * from './src/bolder.component';
export * from './src/bolder.module';
`,
src: {
'bolder.component.ts': `
import {Component, Input} from '@angular/core';
@Component({
selector: 'bolder',
templateUrl: './bolder.component.html'
})
export class BolderComponent {
@Input() data: string;
}
`,
'bolder.component.html': `
<b>{{data}}</b>
`,
'bolder.module.ts': `
import {NgModule} from '@angular/core';
import {BolderComponent} from './bolder.component';
@NgModule({
declarations: [BolderComponent],
exports: [BolderComponent]
})
export class BolderModule {}
`
}
}
};
const LIBRARY_USING_APP_MODULE = ['/lib-user/app/app.module.ts'];
const LIBRARY_USING_APP: MockData = {
'lib-user': {
app: {
'app.component.ts': `
import {Component} from '@angular/core';
@Component({
template: '<h1>Hello <bolder [data]="name"></bolder></h1>'
})
export class AppComponent {
name = 'Angular';
}
`,
'app.module.ts': `
import { NgModule } from '@angular/core';
import { BolderModule } from 'bolder';
import { AppComponent } from './app.component';
@NgModule({
declarations: [ AppComponent ],
bootstrap: [ AppComponent ],
imports: [ BolderModule ]
})
export class AppModule { }
`
}
}
};
function expectPromiseToThrow(p: Promise<any>, msg: RegExp) {
p.then(
() => { throw new Error('Expected to throw'); }, (e) => { expect(e.message).toMatch(msg); });

View File

@ -41,7 +41,10 @@ export const settings: ts.CompilerOptions = {
types: []
};
export interface EmitterOptions { emitMetadata: boolean; }
export interface EmitterOptions {
emitMetadata: boolean;
mockData?: MockData;
}
export class EmittingCompilerHost implements ts.CompilerHost {
private angularSourcePath: string|undefined;
@ -100,12 +103,14 @@ export class EmittingCompilerHost implements ts.CompilerHost {
// ts.ModuleResolutionHost
fileExists(fileName: string): boolean {
return this.addedFiles.has(fileName) || fs.existsSync(fileName);
return this.addedFiles.has(fileName) || open(fileName, this.options.mockData) != null ||
fs.existsSync(fileName);
}
readFile(fileName: string): string {
const result = this.addedFiles.get(fileName);
const result = this.addedFiles.get(fileName) || open(fileName, this.options.mockData);
if (result) return result;
let basename = path.basename(fileName);
if (/^lib.*\.d\.ts$/.test(basename)) {
let libPath = ts.getDefaultLibFilePath(settings);
@ -115,12 +120,17 @@ export class EmittingCompilerHost implements ts.CompilerHost {
}
directoryExists(directoryName: string): boolean {
return fs.existsSync(directoryName) && fs.statSync(directoryName).isDirectory();
return directoryExists(directoryName, this.options.mockData) ||
(fs.existsSync(directoryName) && fs.statSync(directoryName).isDirectory());
}
getCurrentDirectory(): string { return this.root; }
getDirectories(dir: string): string[] {
const result = open(dir, this.options.mockData);
if (result && typeof result !== 'string') {
return Object.keys(result);
}
return fs.readdirSync(dir).filter(p => {
const name = path.join(dir, p);
const stat = fs.statSync(name);
@ -160,6 +170,8 @@ export class EmittingCompilerHost implements ts.CompilerHost {
getNewLine(): string { return '\n'; }
}
const MOCK_NODEMODULES_PREFIX = '/node_modules/';
export class MockCompilerHost implements ts.CompilerHost {
scriptNames: string[];
@ -171,7 +183,9 @@ export class MockCompilerHost implements ts.CompilerHost {
private assumeExists = new Set<string>();
private traces: string[] = [];
constructor(scriptNames: string[], private data: MockData, private angular: Map<string, string>) {
constructor(
scriptNames: string[], private data: MockData, private angular: Map<string, string>,
private libraries?: Map<string, string>[]) {
this.scriptNames = scriptNames.slice(0);
const moduleFilename = module.filename.replace(/\\/g, '/');
let angularIndex = moduleFilename.indexOf('@angular');
@ -219,13 +233,21 @@ export class MockCompilerHost implements ts.CompilerHost {
const effectiveName = this.getEffectiveName(fileName);
if (effectiveName == fileName) {
let result = open(fileName, this.data) != null;
if (!result && fileName.startsWith(MOCK_NODEMODULES_PREFIX)) {
const libraryPath = fileName.substr(MOCK_NODEMODULES_PREFIX.length - 1);
for (const library of this.libraries) {
if (library.has(libraryPath)) {
return true;
}
}
}
return result;
} else {
if (fileName.match(rxjs)) {
let result = fs.existsSync(effectiveName);
return result;
}
let result = this.angular.has(effectiveName);
const result = this.angular.has(effectiveName);
return result;
}
}
@ -292,9 +314,16 @@ export class MockCompilerHost implements ts.CompilerHost {
return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
} else {
let effectiveName = this.getEffectiveName(fileName);
if (effectiveName === fileName)
return open(fileName, this.data);
else {
if (effectiveName === fileName) {
const result = open(fileName, this.data);
if (!result && fileName.startsWith(MOCK_NODEMODULES_PREFIX)) {
const libraryPath = fileName.substr(MOCK_NODEMODULES_PREFIX.length - 1);
for (const library of this.libraries) {
if (library.has(libraryPath)) return library.get(libraryPath);
}
}
return result;
} else {
if (fileName.match(rxjs)) {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf8');
@ -391,7 +420,11 @@ export class MockAotCompilerHost implements AotCompilerHost {
}
loadResource(path: string): Promise<string> {
return Promise.resolve(this.tsHost.readFile(path));
if (this.tsHost.fileExists(path)) {
return Promise.resolve(this.tsHost.readFile(path));
} else {
return Promise.reject(new Error(`Resource ${path} not found.`))
}
}
}
@ -407,6 +440,7 @@ export class MockMetadataBundlerHost implements MetadataBundlerHost {
}
function find(fileName: string, data: MockData): MockData|undefined {
if (!data) return undefined;
let names = fileName.split('/');
if (names.length && !names[0].length) names.shift();
let current = data;