Skip to content

Commit d2e22e9

Browse files
Alan Agiusmgechev
authored andcommitted
feat(@ngtools/webpack): add NGCC as part of the workflow
When add module is resolved, it will try to convert the module using the `NGCC` API. NGCC will be run hooked to the compiler during the module resolution, using the Compiler Host methods 'resolveTypeReferenceDirectives' and 'resolveModuleNames'. It will process a single entry for each module and is based on the first match from the Webpack mainFields. When Ivy is enabled we also append the '_ivy_ngcc' suffixed properties to the mainFields so that Webpack resolver will resolve ngcc processed modules first.
1 parent 655626c commit d2e22e9

File tree

6 files changed

+233
-9
lines changed

6 files changed

+233
-9
lines changed

packages/ngtools/webpack/src/angular_compiler_plugin.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
PLATFORM,
4646
} from './interfaces';
4747
import { LazyRouteMap, findLazyRoutes } from './lazy_routes';
48+
import { NgccProcessor } from './ngcc_processor';
4849
import { TypeScriptPathsPlugin } from './paths-plugin';
4950
import { WebpackResourceLoader } from './resource_loader';
5051
import {
@@ -68,7 +69,7 @@ import {
6869
MESSAGE_KIND,
6970
UpdateMessage,
7071
} from './type_checker_messages';
71-
import { workaroundResolve } from './utils';
72+
import { flattenArray, workaroundResolve } from './utils';
7273
import {
7374
VirtualFileSystemDecorator,
7475
VirtualWatchFileSystemDecorator,
@@ -123,6 +124,8 @@ export class AngularCompilerPlugin {
123124
// Logging.
124125
private _logger: logging.Logger;
125126

127+
private _mainFields: string[] = [];
128+
126129
constructor(options: AngularCompilerPluginOptions) {
127130
this._options = Object.assign({}, options);
128131
this._setupOptions(this._options);
@@ -594,6 +597,16 @@ export class AngularCompilerPlugin {
594597
// Registration hook for webpack plugin.
595598
// tslint:disable-next-line:no-big-function
596599
apply(compiler: Compiler & { watchMode?: boolean }) {
600+
// The below is require by NGCC processor
601+
// since we need to know which fields we need to process
602+
compiler.hooks.environment.tap('angular-compiler', () => {
603+
const { options } = compiler;
604+
const mainFields = options.resolve && options.resolve.mainFields;
605+
if (mainFields) {
606+
this._mainFields = flattenArray(mainFields);
607+
}
608+
});
609+
597610
// cleanup if not watching
598611
compiler.hooks.thisCompilation.tap('angular-compiler', compilation => {
599612
compilation.hooks.finishModules.tap('angular-compiler', () => {
@@ -645,14 +658,33 @@ export class AngularCompilerPlugin {
645658
}
646659
}
647660

661+
let ngccProcessor: NgccProcessor | undefined;
662+
if (this._compilerOptions.enableIvy) {
663+
let ngcc: typeof import('@angular/compiler-cli/ngcc') | undefined;
664+
try {
665+
// this is done for the sole reason that @ngtools/webpack
666+
// support versions of Angular that don't have NGCC API
667+
ngcc = require('@angular/compiler-cli/ngcc');
668+
} catch {
669+
}
670+
671+
if (ngcc) {
672+
ngccProcessor = new NgccProcessor(
673+
ngcc,
674+
this._mainFields,
675+
compilerWithFileSystems.inputFileSystem,
676+
);
677+
}
678+
}
679+
648680
// Create the webpack compiler host.
649681
const webpackCompilerHost = new WebpackCompilerHost(
650682
this._compilerOptions,
651683
this._basePath,
652684
host,
653685
true,
654686
this._options.directTemplateLoading,
655-
this._platform,
687+
ngccProcessor,
656688
);
657689

658690
// Create and set a new WebpackResourceLoader in AOT
@@ -764,6 +796,26 @@ export class AngularCompilerPlugin {
764796
});
765797

766798
compiler.hooks.afterResolvers.tap('angular-compiler', compiler => {
799+
if (this._compilerOptions.enableIvy) {
800+
// When Ivy is enabled we need to add the fields added by NGCC
801+
// to take precedence over the provided mainFields.
802+
// NGCC adds fields in package.json suffixed with '_ivy_ngcc'
803+
// Example: module -> module__ivy_ngcc
804+
// tslint:disable-next-line:no-any
805+
(compiler as any).resolverFactory.hooks.resolveOptions
806+
.for('normal')
807+
// tslint:disable-next-line:no-any
808+
.tap('WebpackOptionsApply', (resolveOptions: any) => {
809+
const mainFields = (resolveOptions.mainFields as string[])
810+
.map(f => [`${f}_ivy_ngcc`, f]);
811+
812+
return {
813+
...resolveOptions,
814+
mainFields: flattenArray(mainFields),
815+
};
816+
});
817+
}
818+
767819
// tslint:disable-next-line:no-any
768820
(compiler as any).resolverFactory.hooks.resolver
769821
.for('normal')

packages/ngtools/webpack/src/compiler_host.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
} from '@angular-devkit/core';
1616
import { Stats } from 'fs';
1717
import * as ts from 'typescript';
18+
import { NgccProcessor } from './ngcc_processor';
1819
import { WebpackResourceLoader } from './resource_loader';
20+
import { workaroundResolve } from './utils';
1921

2022

2123
export interface OnErrorFn {
@@ -47,6 +49,7 @@ export class WebpackCompilerHost implements ts.CompilerHost {
4749
host: virtualFs.Host,
4850
private readonly cacheSourceFiles: boolean,
4951
private readonly directTemplateLoading = false,
52+
private readonly ngccProcessor?: NgccProcessor,
5053
) {
5154
this._syncHost = new virtualFs.SyncDelegateHost(host);
5255
this._memoryHost = new virtualFs.SyncDelegateHost(new virtualFs.SimpleMemoryHost());
@@ -354,13 +357,46 @@ export class WebpackCompilerHost implements ts.CompilerHost {
354357
trace(message: string) {
355358
console.log(message);
356359
}
357-
}
358360

361+
resolveModuleNames(
362+
moduleNames: string[],
363+
containingFile: string,
364+
): (ts.ResolvedModule | undefined)[] {
365+
return moduleNames.map(moduleName => {
366+
const { resolvedModule } = ts.resolveModuleName(
367+
moduleName,
368+
workaroundResolve(containingFile),
369+
this._options,
370+
this,
371+
);
372+
373+
if (this._options.enableIvy && resolvedModule && this.ngccProcessor) {
374+
this.ngccProcessor.processModule(moduleName, resolvedModule);
375+
}
376+
377+
return resolvedModule;
378+
});
379+
}
380+
381+
resolveTypeReferenceDirectives(
382+
typeReferenceDirectiveNames: string[],
383+
containingFile: string,
384+
redirectedReference?: ts.ResolvedProjectReference,
385+
): (ts.ResolvedTypeReferenceDirective | undefined)[] {
386+
return typeReferenceDirectiveNames.map(moduleName => {
387+
const { resolvedTypeReferenceDirective } = ts.resolveTypeReferenceDirective(
388+
moduleName,
389+
workaroundResolve(containingFile),
390+
this._options,
391+
this,
392+
redirectedReference,
393+
);
394+
395+
if (this._options.enableIvy && resolvedTypeReferenceDirective && this.ngccProcessor) {
396+
this.ngccProcessor.processModule(moduleName, resolvedTypeReferenceDirective);
397+
}
359398

360-
// `TsCompilerAotCompilerTypeCheckHostAdapter` in @angular/compiler-cli seems to resolve module
361-
// names directly via `resolveModuleName`, which prevents full Path usage.
362-
// To work around this we must provide the same path format as TS internally uses in
363-
// the SourceFile paths.
364-
export function workaroundResolve(path: Path | string) {
365-
return getSystemPath(normalize(path)).replace(/\\/g, '/');
399+
return resolvedTypeReferenceDirective;
400+
});
401+
}
366402
}

packages/ngtools/webpack/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
export * from './angular_compiler_plugin';
10+
export * from './interfaces';
1011
export { ngcLoader as default } from './loader';
1112

1213
export const NgToolsLoader = __filename;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
import { InputFileSystem } from 'webpack';
11+
import { time, timeEnd } from './benchmark';
12+
import { workaroundResolve } from './utils';
13+
14+
// We cannot create a plugin for this, because NGTSC requires addition type
15+
// information which ngcc creates when processing a package which was compiled with NGC.
16+
17+
// Example of such errors:
18+
// ERROR in node_modules/@angular/platform-browser/platform-browser.d.ts(42,22):
19+
// error TS-996002: Appears in the NgModule.imports of AppModule,
20+
// but could not be resolved to an NgModule class
21+
22+
// We now transform a package and it's typings when NGTSC is resolving a module.
23+
24+
export class NgccProcessor {
25+
private _processedModules = new Set<string>();
26+
27+
constructor(
28+
private readonly ngcc: typeof import('@angular/compiler-cli/ngcc'),
29+
private readonly propertiesToConsider: string[],
30+
private readonly inputFileSystem: InputFileSystem,
31+
) {
32+
}
33+
34+
processModule(
35+
moduleName: string,
36+
resolvedModule: ts.ResolvedModule | ts.ResolvedTypeReferenceDirective,
37+
): void {
38+
const resolvedFileName = resolvedModule.resolvedFileName;
39+
if (
40+
!resolvedFileName
41+
|| moduleName.startsWith('.')
42+
|| this._processedModules.has(moduleName)) {
43+
// Skip when module is unknown, relative or NGCC compiler is not found or already processed.
44+
return;
45+
}
46+
47+
const packageJsonPath = this.tryResolvePackage(moduleName, resolvedFileName);
48+
if (!packageJsonPath) {
49+
// add it to processed so the second time round we skip this.
50+
this._processedModules.add(moduleName);
51+
52+
return;
53+
}
54+
const normalizedJsonPath = workaroundResolve(packageJsonPath);
55+
56+
const timeLabel = `NgccProcessor.processModule.ngcc.process+${moduleName}`;
57+
time(timeLabel);
58+
this.ngcc.process({
59+
basePath: normalizedJsonPath.substring(0, normalizedJsonPath.indexOf(moduleName)),
60+
targetEntryPointPath: moduleName,
61+
propertiesToConsider: this.propertiesToConsider,
62+
compileAllFormats: false,
63+
createNewEntryPointFormats: true,
64+
});
65+
timeEnd(timeLabel);
66+
67+
// Purge this file from cache, since NGCC add new mainFields. Ex: module_ivy_ngcc
68+
// which are unknown in the cached file.
69+
70+
// tslint:disable-next-line:no-any
71+
(this.inputFileSystem as any).purge(packageJsonPath);
72+
73+
this._processedModules.add(moduleName);
74+
}
75+
76+
/**
77+
* Try resolve a package.json file from the resolved .d.ts file.
78+
*/
79+
private tryResolvePackage(moduleName: string, resolvedFileName: string): string | undefined {
80+
try {
81+
// This is based on the logic in the NGCC compiler
82+
// tslint:disable-next-line:max-line-length
83+
// See: https://github.com/angular/angular/blob/b93c1dffa17e4e6900b3ab1b9e554b6da92be0de/packages/compiler-cli/src/ngcc/src/packages/dependency_host.ts#L85-L121
84+
const packageJsonPath = require.resolve(`${moduleName}/package.json`,
85+
{
86+
paths: [resolvedFileName],
87+
},
88+
);
89+
90+
return packageJsonPath;
91+
} catch {
92+
// if it fails this might be a deep import which doesn't have a package.json
93+
// Ex: @angular/compiler/src/i18n/i18n_ast/package.json
94+
return undefined;
95+
}
96+
}
97+
}

packages/ngtools/webpack/src/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { Path, getSystemPath, normalize } from '@angular-devkit/core';
9+
10+
// `TsCompilerAotCompilerTypeCheckHostAdapter` in @angular/compiler-cli seems to resolve module
11+
// names directly via `resolveModuleName`, which prevents full Path usage.
12+
// To work around this we must provide the same path format as TS internally uses in
13+
// the SourceFile paths.
14+
export function workaroundResolve(path: Path | string) {
15+
return getSystemPath(normalize(path)).replace(/\\/g, '/');
16+
}
17+
18+
export function flattenArray<T>(value: Array<T | T[]>): T[] {
19+
return [].concat.apply([], value);
20+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { flattenArray } from './utils';
10+
11+
describe('@ngtools/webpack utils', () => {
12+
describe('flattenArray', () => {
13+
it('should flatten an array', () => {
14+
const arr = flattenArray(['module', ['browser', 'main']]);
15+
expect(arr).toEqual(['module', 'browser', 'main']);
16+
});
17+
});
18+
});

0 commit comments

Comments
 (0)