fix(compiler-cli): Use typescript to resolve modules for metadata (#22856)
The current module resolution simply attaches .ts to the import/export path, which does not work if the path is using Node / CommonJS behavior to resolve to an index.ts file. This patch uses typescript's module resolution logic, and will attempt to load the original typescript file if this resolution returns a .js or .d.ts file PR Close #22856
This commit is contained in:
parent
a167bca927
commit
0d5f2d3c7e
|
@ -103,7 +103,7 @@ export function createBundleIndexHost<H extends ts.CompilerHost>(
|
||||||
// etc.
|
// etc.
|
||||||
const getMetadataBundle = (cache: MetadataCache | null) => {
|
const getMetadataBundle = (cache: MetadataCache | null) => {
|
||||||
const bundler = new MetadataBundler(
|
const bundler = new MetadataBundler(
|
||||||
indexModule, ngOptions.flatModuleId, new CompilerHostAdapter(host, cache),
|
indexModule, ngOptions.flatModuleId, new CompilerHostAdapter(host, cache, ngOptions),
|
||||||
ngOptions.flatModulePrivateSymbolPrefix);
|
ngOptions.flatModulePrivateSymbolPrefix);
|
||||||
return bundler.getMetadataBundle();
|
return bundler.getMetadataBundle();
|
||||||
};
|
};
|
||||||
|
|
|
@ -72,7 +72,7 @@ export interface BundledModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetadataBundlerHost {
|
export interface MetadataBundlerHost {
|
||||||
getMetadataFor(moduleName: string): ModuleMetadata|undefined;
|
getMetadataFor(moduleName: string, containingFile: string): ModuleMetadata|undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
type StaticsMetadata = {
|
type StaticsMetadata = {
|
||||||
|
@ -136,7 +136,7 @@ export class MetadataBundler {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
if (moduleName.startsWith('.')) {
|
if (moduleName.startsWith('.')) {
|
||||||
const fullModuleName = resolveModule(moduleName, this.root);
|
const fullModuleName = resolveModule(moduleName, this.root);
|
||||||
result = this.host.getMetadataFor(fullModuleName);
|
result = this.host.getMetadataFor(fullModuleName, this.root);
|
||||||
}
|
}
|
||||||
this.metadataCache.set(moduleName, result);
|
this.metadataCache.set(moduleName, result);
|
||||||
}
|
}
|
||||||
|
@ -598,11 +598,27 @@ export class MetadataBundler {
|
||||||
export class CompilerHostAdapter implements MetadataBundlerHost {
|
export class CompilerHostAdapter implements MetadataBundlerHost {
|
||||||
private collector = new MetadataCollector();
|
private collector = new MetadataCollector();
|
||||||
|
|
||||||
constructor(private host: ts.CompilerHost, private cache: MetadataCache|null) {}
|
constructor(
|
||||||
|
private host: ts.CompilerHost, private cache: MetadataCache|null,
|
||||||
|
private options: ts.CompilerOptions) {}
|
||||||
|
|
||||||
getMetadataFor(fileName: string): ModuleMetadata|undefined {
|
getMetadataFor(fileName: string, containingFile: string): ModuleMetadata|undefined {
|
||||||
|
const {resolvedModule} =
|
||||||
|
ts.resolveModuleName(fileName, containingFile, this.options, this.host);
|
||||||
|
|
||||||
|
let sourceFile: ts.SourceFile|undefined;
|
||||||
|
if (resolvedModule) {
|
||||||
|
let {resolvedFileName} = resolvedModule;
|
||||||
|
if (resolvedModule.extension !== '.ts') {
|
||||||
|
resolvedFileName = resolvedFileName.replace(/(\.d\.ts|\.js)$/, '.ts');
|
||||||
|
}
|
||||||
|
sourceFile = this.host.getSourceFile(resolvedFileName, ts.ScriptTarget.Latest);
|
||||||
|
} else {
|
||||||
|
// If typescript is unable to resolve the file, fallback on old behavior
|
||||||
if (!this.host.fileExists(fileName + '.ts')) return undefined;
|
if (!this.host.fileExists(fileName + '.ts')) return undefined;
|
||||||
const sourceFile = this.host.getSourceFile(fileName + '.ts', ts.ScriptTarget.Latest);
|
sourceFile = this.host.getSourceFile(fileName + '.ts', ts.ScriptTarget.Latest);
|
||||||
|
}
|
||||||
|
|
||||||
// If there is a metadata cache, use it to get the metadata for this source file. Otherwise,
|
// If there is a metadata cache, use it to get the metadata for this source file. Otherwise,
|
||||||
// fall back on the locally created MetadataCollector.
|
// fall back on the locally created MetadataCollector.
|
||||||
if (!sourceFile) {
|
if (!sourceFile) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ ts_library(
|
||||||
"//packages:types",
|
"//packages:types",
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
"//packages/compiler-cli",
|
"//packages/compiler-cli",
|
||||||
|
"//packages/compiler-cli/test:test_utils",
|
||||||
"//packages/core",
|
"//packages/core",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,11 +9,187 @@
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {MetadataBundler, MetadataBundlerHost} from '../../src/metadata/bundler';
|
import {CompilerHostAdapter, MetadataBundler, MetadataBundlerHost} from '../../src/metadata/bundler';
|
||||||
import {MetadataCollector} from '../../src/metadata/collector';
|
import {MetadataCollector} from '../../src/metadata/collector';
|
||||||
import {ClassMetadata, MetadataGlobalReferenceExpression, ModuleMetadata} from '../../src/metadata/schema';
|
import {ClassMetadata, MetadataGlobalReferenceExpression, ModuleMetadata} from '../../src/metadata/schema';
|
||||||
|
import {Directory, MockAotContext, MockCompilerHost} from '../mocks';
|
||||||
|
|
||||||
import {Directory, open} from './typescript.mocks';
|
describe('compiler host adapter', () => {
|
||||||
|
|
||||||
|
it('should retrieve metadata for an explicit index relative path reference', () => {
|
||||||
|
const context = new MockAotContext('.', SIMPLE_LIBRARY);
|
||||||
|
const host = new MockCompilerHost(context);
|
||||||
|
const options: ts.CompilerOptions = {
|
||||||
|
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES5,
|
||||||
|
};
|
||||||
|
const adapter = new CompilerHostAdapter(host, null, options);
|
||||||
|
const metadata = adapter.getMetadataFor('./lib/src/two/index', '.');
|
||||||
|
|
||||||
|
expect(metadata).toBeDefined();
|
||||||
|
expect(Object.keys(metadata !.metadata).sort()).toEqual([
|
||||||
|
'PrivateTwo',
|
||||||
|
'TWO_CLASSES',
|
||||||
|
'Two',
|
||||||
|
'TwoMore',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve metadata for an implied index relative path reference', () => {
|
||||||
|
const context = new MockAotContext('.', SIMPLE_LIBRARY_WITH_IMPLIED_INDEX);
|
||||||
|
const host = new MockCompilerHost(context);
|
||||||
|
const options: ts.CompilerOptions = {
|
||||||
|
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES5,
|
||||||
|
};
|
||||||
|
const adapter = new CompilerHostAdapter(host, null, options);
|
||||||
|
const metadata = adapter.getMetadataFor('./lib/src/two', '.');
|
||||||
|
|
||||||
|
expect(metadata).toBeDefined();
|
||||||
|
expect(Object.keys(metadata !.metadata).sort()).toEqual([
|
||||||
|
'PrivateTwo',
|
||||||
|
'TWO_CLASSES',
|
||||||
|
'Two',
|
||||||
|
'TwoMore',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail to retrieve metadata for an implied index with classic module resolution', () => {
|
||||||
|
const context = new MockAotContext('.', SIMPLE_LIBRARY_WITH_IMPLIED_INDEX);
|
||||||
|
const host = new MockCompilerHost(context);
|
||||||
|
const options: ts.CompilerOptions = {
|
||||||
|
moduleResolution: ts.ModuleResolutionKind.Classic,
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES5,
|
||||||
|
};
|
||||||
|
const adapter = new CompilerHostAdapter(host, null, options);
|
||||||
|
const metadata = adapter.getMetadataFor('./lib/src/two', '.');
|
||||||
|
|
||||||
|
expect(metadata).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve exports for an explicit index relative path reference', () => {
|
||||||
|
const context = new MockAotContext('.', SIMPLE_LIBRARY);
|
||||||
|
const host = new MockCompilerHost(context);
|
||||||
|
const options: ts.CompilerOptions = {
|
||||||
|
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES5,
|
||||||
|
};
|
||||||
|
const adapter = new CompilerHostAdapter(host, null, options);
|
||||||
|
const metadata = adapter.getMetadataFor('./lib/src/index', '.');
|
||||||
|
|
||||||
|
expect(metadata).toBeDefined();
|
||||||
|
expect(metadata !.exports !.map(e => e.export !)
|
||||||
|
.reduce((prev, next) => prev.concat(next), [])
|
||||||
|
.sort())
|
||||||
|
.toEqual([
|
||||||
|
'ONE_CLASSES',
|
||||||
|
'One',
|
||||||
|
'OneMore',
|
||||||
|
'TWO_CLASSES',
|
||||||
|
'Two',
|
||||||
|
'TwoMore',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should look for .ts file when resolving metadata via a package.json "main" entry', () => {
|
||||||
|
const files = {
|
||||||
|
'lib': {
|
||||||
|
'one.ts': `
|
||||||
|
class One {}
|
||||||
|
class OneMore extends One {}
|
||||||
|
class PrivateOne {}
|
||||||
|
const ONE_CLASSES = [One, OneMore, PrivateOne];
|
||||||
|
export {One, OneMore, PrivateOne, ONE_CLASSES};
|
||||||
|
`,
|
||||||
|
'one.js': `
|
||||||
|
// This will throw an error if the metadata collector tries to load one.js
|
||||||
|
`,
|
||||||
|
'package.json': `
|
||||||
|
{
|
||||||
|
"main": "one"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = new MockAotContext('.', files);
|
||||||
|
const host = new MockCompilerHost(context);
|
||||||
|
const options: ts.CompilerOptions = {
|
||||||
|
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES5,
|
||||||
|
};
|
||||||
|
const adapter = new CompilerHostAdapter(host, null, options);
|
||||||
|
const metadata = adapter.getMetadataFor('./lib', '.');
|
||||||
|
|
||||||
|
expect(metadata).toBeDefined();
|
||||||
|
expect(Object.keys(metadata !.metadata).sort()).toEqual([
|
||||||
|
'ONE_CLASSES',
|
||||||
|
'One',
|
||||||
|
'OneMore',
|
||||||
|
'PrivateOne',
|
||||||
|
]);
|
||||||
|
expect(Array.isArray(metadata !.metadata !['ONE_CLASSES'])).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should look for non-declaration file when resolving metadata via a package.json "types" entry',
|
||||||
|
() => {
|
||||||
|
const files = {
|
||||||
|
'lib': {
|
||||||
|
'one.ts': `
|
||||||
|
class One {}
|
||||||
|
class OneMore extends One {}
|
||||||
|
class PrivateOne {}
|
||||||
|
const ONE_CLASSES = [One, OneMore, PrivateOne];
|
||||||
|
export {One, OneMore, PrivateOne, ONE_CLASSES};
|
||||||
|
`,
|
||||||
|
'one.d.ts': `
|
||||||
|
declare class One {
|
||||||
|
}
|
||||||
|
declare class OneMore extends One {
|
||||||
|
}
|
||||||
|
declare class PrivateOne {
|
||||||
|
}
|
||||||
|
declare const ONE_CLASSES: (typeof One)[];
|
||||||
|
export { One, OneMore, PrivateOne, ONE_CLASSES };
|
||||||
|
`,
|
||||||
|
'one.js': `
|
||||||
|
// This will throw an error if the metadata collector tries to load one.js
|
||||||
|
`,
|
||||||
|
'package.json': `
|
||||||
|
{
|
||||||
|
"main": "one",
|
||||||
|
"types": "one.d.ts"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = new MockAotContext('.', files);
|
||||||
|
const host = new MockCompilerHost(context);
|
||||||
|
const options: ts.CompilerOptions = {
|
||||||
|
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES5,
|
||||||
|
};
|
||||||
|
const adapter = new CompilerHostAdapter(host, null, options);
|
||||||
|
const metadata = adapter.getMetadataFor('./lib', '.');
|
||||||
|
|
||||||
|
expect(metadata).toBeDefined();
|
||||||
|
expect(Object.keys(metadata !.metadata).sort()).toEqual([
|
||||||
|
'ONE_CLASSES',
|
||||||
|
'One',
|
||||||
|
'OneMore',
|
||||||
|
'PrivateOne',
|
||||||
|
]);
|
||||||
|
expect(Array.isArray(metadata !.metadata !['ONE_CLASSES'])).toBeTruthy();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('metadata bundler', () => {
|
describe('metadata bundler', () => {
|
||||||
|
|
||||||
|
@ -231,25 +407,23 @@ describe('metadata bundler', () => {
|
||||||
|
|
||||||
export class MockStringBundlerHost implements MetadataBundlerHost {
|
export class MockStringBundlerHost implements MetadataBundlerHost {
|
||||||
collector = new MetadataCollector();
|
collector = new MetadataCollector();
|
||||||
|
adapter: CompilerHostAdapter;
|
||||||
|
|
||||||
constructor(private dirName: string, private directory: Directory) {}
|
constructor(private dirName: string, directory: Directory) {
|
||||||
|
const context = new MockAotContext(dirName, directory);
|
||||||
|
const host = new MockCompilerHost(context);
|
||||||
|
const options = {
|
||||||
|
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES5,
|
||||||
|
};
|
||||||
|
this.adapter = new CompilerHostAdapter(host, null, options);
|
||||||
|
}
|
||||||
|
|
||||||
getMetadataFor(moduleName: string): ModuleMetadata|undefined {
|
getMetadataFor(moduleName: string): ModuleMetadata|undefined {
|
||||||
const fileName = path.join(this.dirName, moduleName) + '.ts';
|
return this.adapter.getMetadataFor(moduleName, this.dirName);
|
||||||
const text = open(this.directory, fileName);
|
|
||||||
if (typeof text == 'string') {
|
|
||||||
const sourceFile = ts.createSourceFile(
|
|
||||||
fileName, text, ts.ScriptTarget.Latest, /* setParent */ true, ts.ScriptKind.TS);
|
|
||||||
const diagnostics: ts.Diagnostic[] = (sourceFile as any).parseDiagnostics;
|
|
||||||
if (diagnostics && diagnostics.length) {
|
|
||||||
throw Error('Unexpected syntax error in test');
|
|
||||||
}
|
|
||||||
const result = this.collector.getMetadata(sourceFile);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const SIMPLE_LIBRARY = {
|
export const SIMPLE_LIBRARY = {
|
||||||
'lib': {
|
'lib': {
|
||||||
|
@ -278,3 +452,31 @@ export const SIMPLE_LIBRARY = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SIMPLE_LIBRARY_WITH_IMPLIED_INDEX = {
|
||||||
|
'lib': {
|
||||||
|
'index.ts': `
|
||||||
|
export * from './src';
|
||||||
|
`,
|
||||||
|
'src': {
|
||||||
|
'index.ts': `
|
||||||
|
export {One, OneMore, ONE_CLASSES} from './one';
|
||||||
|
export {Two, TwoMore, TWO_CLASSES} from './two';
|
||||||
|
`,
|
||||||
|
'one.ts': `
|
||||||
|
export class One {}
|
||||||
|
export class OneMore extends One {}
|
||||||
|
export class PrivateOne {}
|
||||||
|
export const ONE_CLASSES = [One, OneMore, PrivateOne];
|
||||||
|
`,
|
||||||
|
'two': {
|
||||||
|
'index.ts': `
|
||||||
|
export class Two {}
|
||||||
|
export class TwoMore extends Two {}
|
||||||
|
export class PrivateTwo {}
|
||||||
|
export const TWO_CLASSES = [Two, TwoMore, PrivateTwo];
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -19,7 +19,9 @@ export class MockAotContext {
|
||||||
|
|
||||||
fileExists(fileName: string): boolean { return typeof this.getEntry(fileName) === 'string'; }
|
fileExists(fileName: string): boolean { return typeof this.getEntry(fileName) === 'string'; }
|
||||||
|
|
||||||
directoryExists(path: string): boolean { return typeof this.getEntry(path) === 'object'; }
|
directoryExists(path: string): boolean {
|
||||||
|
return path === this.currentDirectory || typeof this.getEntry(path) === 'object';
|
||||||
|
}
|
||||||
|
|
||||||
readFile(fileName: string): string {
|
readFile(fileName: string): string {
|
||||||
const data = this.getEntry(fileName);
|
const data = this.getEntry(fileName);
|
||||||
|
|
Loading…
Reference in New Issue