Skip to content

Commit 9799f3f

Browse files
committed
fix(@ngtools/webpack): analyze the typescript file for additional dependencies
By default, Webpack only add dependencies it can see. Types or imports that are not kept in transpilations are removed, and webpack dont see them. By reading the AST directly we manually add the dependencies to webpack. Fixes #7995.
1 parent 6386633 commit 9799f3f

File tree

4 files changed

+100
-7
lines changed

4 files changed

+100
-7
lines changed

packages/@ngtools/webpack/src/angular_compiler_plugin.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
formatDiagnostics,
4444
EmitFlags,
4545
} from './ngtools_api';
46+
import { findAstNodes } from './transformers/ast_helpers';
4647

4748

4849
/**
@@ -85,6 +86,7 @@ export class AngularCompilerPlugin implements Tapable {
8586
private _tsFilenames: string[];
8687
private _program: ts.Program | Program;
8788
private _compilerHost: WebpackCompilerHost;
89+
private _moduleResolutionCache: ts.ModuleResolutionCache;
8890
private _angularCompilerHost: WebpackCompilerHost & CompilerHost;
8991
private _resourceLoader: WebpackResourceLoader;
9092
// Contains `moduleImportPath#exportName` => `fullModulePath`.
@@ -296,6 +298,9 @@ export class AngularCompilerPlugin implements Tapable {
296298
}
297299
}
298300

301+
// Use an identity function as all our paths are absolute already.
302+
this._moduleResolutionCache = ts.createModuleResolutionCache(this._basePath, x => x);
303+
299304
// Set platform.
300305
this._platform = options.platform || PLATFORM.Browser;
301306
timeEnd('AngularCompilerPlugin._setupOptions');
@@ -775,6 +780,26 @@ export class AngularCompilerPlugin implements Tapable {
775780
};
776781
}
777782

783+
getDependencies(fileName: string): string[] {
784+
const sourceFile = this._compilerHost.getSourceFile(fileName, ts.ScriptTarget.Latest);
785+
const options = this._compilerOptions;
786+
const host = this._compilerHost;
787+
const cache = this._moduleResolutionCache;
788+
789+
return findAstNodes<ts.ImportDeclaration>(null, sourceFile, ts.SyntaxKind.ImportDeclaration)
790+
.map(decl => {
791+
const moduleName = (decl.moduleSpecifier as ts.StringLiteral).text;
792+
const resolved = ts.resolveModuleName(moduleName, fileName, options, host, cache);
793+
794+
if (resolved.resolvedModule) {
795+
return resolved.resolvedModule.resolvedFileName;
796+
} else {
797+
return null;
798+
}
799+
})
800+
.filter(x => x);
801+
}
802+
778803
// This code mostly comes from `performCompilation` in `@angular/compiler-cli`.
779804
// It skips the program creation because we need to use `loadNgStructureAsync()`,
780805
// and uses CustomTransformers.

packages/@ngtools/webpack/src/loader.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,28 @@ function _getResourcesUrls(refactor: TypeScriptFileRefactor): string[] {
431431
}
432432

433433

434+
function _getImports(refactor: TypeScriptFileRefactor,
435+
compilerOptions: ts.CompilerOptions,
436+
host: ts.ModuleResolutionHost,
437+
cache: ts.ModuleResolutionCache): string[] {
438+
const containingFile = refactor.fileName;
439+
440+
return refactor.findAstNodes(null, ts.SyntaxKind.ImportDeclaration, false)
441+
.map((clause: ts.ImportDeclaration) => {
442+
const moduleName = (clause.moduleSpecifier as ts.StringLiteral).text;
443+
const resolved = ts.resolveModuleName(
444+
moduleName, containingFile, compilerOptions, host, cache);
445+
446+
if (resolved.resolvedModule) {
447+
return resolved.resolvedModule.resolvedFileName;
448+
} else {
449+
return null;
450+
}
451+
})
452+
.filter(x => x);
453+
}
454+
455+
434456
/**
435457
* Recursively calls diagnose on the plugins for all the reverse dependencies.
436458
* @private
@@ -546,6 +568,7 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
546568
.then(() => {
547569
timeEnd(timeLabel + '.ngcLoader.AngularCompilerPlugin');
548570
const result = plugin.getFile(sourceFileName);
571+
const dependencies = plugin.getDependencies(sourceFileName);
549572

550573
if (result.sourceMap) {
551574
// Process sourcemaps for Webpack.
@@ -561,6 +584,8 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
561584
if (result.outputText === undefined) {
562585
throw new Error('TypeScript compilation failed.');
563586
}
587+
588+
dependencies.forEach(dep => this.addDependency(dep));
564589
cb(null, result.outputText, result.sourceMap);
565590
})
566591
.catch(err => {
@@ -577,6 +602,12 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
577602
const refactor = new TypeScriptFileRefactor(
578603
sourceFileName, plugin.compilerHost, plugin.program, source);
579604

605+
// Force a few compiler options to make sure we get the result we want.
606+
const compilerOptions: ts.CompilerOptions = Object.assign({}, plugin.compilerOptions, {
607+
inlineSources: true,
608+
inlineSourceMap: false,
609+
sourceRoot: plugin.basePath
610+
});
580611

581612
Promise.resolve()
582613
.then(() => {
@@ -615,6 +646,8 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
615646
_getResourcesUrls(refactor).forEach((url: string) => {
616647
this.addDependency(path.resolve(path.dirname(sourceFileName), url));
617648
});
649+
_getImports(refactor, compilerOptions, plugin.compilerHost, plugin.moduleResolutionCache)
650+
.forEach((importString: string) => this.addDependency(importString));
618651
timeEnd(timeLabel + '.ngcLoader.AotPlugin.addDependency');
619652
})
620653
.then(() => {
@@ -642,13 +675,6 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
642675
timeEnd(timeLabel + '.ngcLoader.AotPlugin.getDiagnostics');
643676
}
644677

645-
// Force a few compiler options to make sure we get the result we want.
646-
const compilerOptions: ts.CompilerOptions = Object.assign({}, plugin.compilerOptions, {
647-
inlineSources: true,
648-
inlineSourceMap: false,
649-
sourceRoot: plugin.basePath
650-
});
651-
652678
time(timeLabel + '.ngcLoader.AotPlugin.transpile');
653679
const result = refactor.transpile(compilerOptions);
654680
timeEnd(timeLabel + '.ngcLoader.AotPlugin.transpile');

packages/@ngtools/webpack/src/plugin.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class AotPlugin implements Tapable {
5151
private _compilerOptions: ts.CompilerOptions;
5252
private _angularCompilerOptions: any;
5353
private _program: ts.Program;
54+
private _moduleResolutionCache: ts.ModuleResolutionCache;
5455
private _rootFilePath: string[];
5556
private _compilerHost: WebpackCompilerHost;
5657
private _resourceLoader: WebpackResourceLoader;
@@ -97,6 +98,7 @@ export class AotPlugin implements Tapable {
9798
}
9899
get genDir() { return this._genDir; }
99100
get program() { return this._program; }
101+
get moduleResolutionCache() { return this._moduleResolutionCache; }
100102
get skipCodeGeneration() { return this._skipCodeGeneration; }
101103
get replaceExport() { return this._replaceExport; }
102104
get typeCheck() { return this._typeCheck; }
@@ -250,6 +252,12 @@ export class AotPlugin implements Tapable {
250252
this._program = ts.createProgram(
251253
this._rootFilePath, this._compilerOptions, this._compilerHost);
252254

255+
// We use absolute paths everywhere.
256+
this._moduleResolutionCache = ts.createModuleResolutionCache(
257+
this._basePath,
258+
(fileName: string) => this._compilerHost.resolve(fileName),
259+
);
260+
253261
// We enable caching of the filesystem in compilerHost _after_ the program has been created,
254262
// because we don't want SourceFile instances to be cached past this point.
255263
this._compilerHost.enableCaching();
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
killAllProcesses,
3+
waitForAnyProcessOutputToMatch,
4+
execAndWaitForOutputToMatch,
5+
} from '../../utils/process';
6+
import { writeFile, prependToFile } from '../../utils/fs';
7+
import {getGlobalVariable} from '../../utils/env';
8+
9+
10+
const successRe = /webpack: Compiled successfully/;
11+
12+
export default async function() {
13+
if (process.platform.startsWith('win')) {
14+
return;
15+
}
16+
// Skip this in ejected tests.
17+
if (getGlobalVariable('argv').eject) {
18+
return;
19+
}
20+
21+
await writeFile('src/app/type.ts', `export type MyType = number;`);
22+
await prependToFile('src/app/app.component.ts', 'import { MyType } from "./type";\n');
23+
24+
try {
25+
await execAndWaitForOutputToMatch('ng', ['serve'], successRe);
26+
27+
await Promise.all([
28+
waitForAnyProcessOutputToMatch(successRe, 20000),
29+
writeFile('src/app/type.ts', `export type MyType = string;`),
30+
]);
31+
} finally {
32+
killAllProcesses();
33+
}
34+
}

0 commit comments

Comments
 (0)