Skip to content

Commit 35d8adf

Browse files
clydinalan-agius4
authored andcommitted
feat(@angular-devkit/build-angular): integrate Angular compiler linker
The newly introduced library linker mode that removes the need for ngcc is integrated into the Angular CLI's build system. This allows libraries that are built in linker mode to be used when building an application.
1 parent 48c4393 commit 35d8adf

File tree

4 files changed

+195
-28
lines changed

4 files changed

+195
-28
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
declare module 'babel-loader' {
9+
type BabelLoaderCustomizer<T> = (
10+
babel: typeof import('@babel/core'),
11+
) => {
12+
customOptions?(
13+
this: import('webpack').loader.LoaderContext,
14+
loaderOptions: Record<string, unknown>,
15+
loaderArguments: { source: string; map?: unknown },
16+
): Promise<{ custom?: T; loader: Record<string, unknown> }>;
17+
config?(
18+
this: import('webpack').loader.LoaderContext,
19+
configuration: import('@babel/core').PartialConfig,
20+
loaderArguments: { source: string; map?: unknown; customOptions: T },
21+
): import('@babel/core').TransformOptions;
22+
};
23+
function custom<T>(customizer: BabelLoaderCustomizer<T>): import('webpack').loader.Loader;
24+
}

packages/angular_devkit/build_angular/src/babel/presets/application.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8+
import * as fs from 'fs';
89
import * as path from 'path';
910

10-
export type DiagnosticReporter = (type: 'error' | 'warning', message: string) => void;
11+
export type DiagnosticReporter = (type: 'error' | 'warning' | 'info', message: string) => void;
1112
export interface ApplicationPresetOptions {
1213
i18n?: {
1314
locale: string;
1415
missingTranslationBehavior?: 'error' | 'warning' | 'ignore';
1516
translation?: unknown;
1617
};
1718

19+
angularLinker?: boolean;
20+
1821
forceES5?: boolean;
1922
forceAsyncTransformation?: boolean;
2023

@@ -98,11 +101,47 @@ function createI18nPlugins(
98101
return plugins;
99102
}
100103

104+
function createNgtscLogger(
105+
reporter: DiagnosticReporter | undefined,
106+
): import('@angular/compiler-cli/src/ngtsc/logging').Logger {
107+
return {
108+
level: 1, // Info level
109+
debug(...args: string[]) {},
110+
info(...args: string[]) {
111+
reporter?.('info', args.join());
112+
},
113+
warn(...args: string[]) {
114+
reporter?.('warning', args.join());
115+
},
116+
error(...args: string[]) {
117+
reporter?.('error', args.join());
118+
},
119+
};
120+
}
121+
101122
export default function (api: unknown, options: ApplicationPresetOptions) {
102123
const presets = [];
103124
const plugins = [];
104125
let needRuntimeTransform = false;
105126

127+
if (options.angularLinker) {
128+
// Babel currently is synchronous so import cannot be used
129+
const {
130+
createEs2015LinkerPlugin,
131+
} = require('@angular/compiler-cli/linker/babel');
132+
133+
plugins.push(createEs2015LinkerPlugin({
134+
logger: createNgtscLogger(options.diagnosticReporter),
135+
fileSystem: {
136+
resolve: path.resolve,
137+
exists: fs.existsSync,
138+
dirname: path.dirname,
139+
relative: path.relative,
140+
readFile: fs.readFileSync,
141+
},
142+
}));
143+
}
144+
106145
if (options.forceES5) {
107146
presets.push([
108147
require('@babel/preset-env').default,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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 { custom } from 'babel-loader';
9+
10+
interface AngularCustomOptions {
11+
forceES5: boolean;
12+
shouldLink: boolean;
13+
}
14+
15+
/**
16+
* Cached linker check utility function
17+
*
18+
* If undefined, not yet been imported
19+
* If null, attempted import failed and no linker support
20+
* If function, import succeeded and linker supported
21+
*/
22+
let needsLinking: undefined | null | typeof import('@angular/compiler-cli/linker').needsLinking;
23+
24+
async function checkLinking(
25+
path: string,
26+
source: string,
27+
): Promise<{ hasLinkerSupport?: boolean; requiresLinking: boolean }> {
28+
// @angular/core and @angular/compiler will cause false positives
29+
if (/[\\\/]@angular[\\\/](?:compiler|core)/.test(path)) {
30+
return { requiresLinking: false };
31+
}
32+
33+
if (needsLinking !== null) {
34+
try {
35+
if (needsLinking === undefined) {
36+
needsLinking = (await import('@angular/compiler-cli/linker')).needsLinking;
37+
}
38+
39+
// If the linker entry point is present then there is linker support
40+
return { hasLinkerSupport: true, requiresLinking: needsLinking(path, source) };
41+
} catch {
42+
needsLinking = null;
43+
}
44+
}
45+
46+
// Fallback for Angular versions less than 11.1.0 with no linker support.
47+
// This information is used to issue errors if a partially compiled library is used when unsupported.
48+
return {
49+
hasLinkerSupport: false,
50+
requiresLinking:
51+
source.includes('ɵɵngDeclareDirective') || source.includes('ɵɵngDeclareComponent'),
52+
};
53+
}
54+
55+
export default custom<AngularCustomOptions>(() => {
56+
const baseOptions = Object.freeze({
57+
babelrc: false,
58+
configFile: false,
59+
compact: false,
60+
cacheCompression: false,
61+
sourceType: 'unambiguous',
62+
});
63+
64+
return {
65+
async customOptions({ forceES5, ...loaderOptions }, { source }) {
66+
let shouldProcess = forceES5;
67+
68+
let shouldLink = false;
69+
const { hasLinkerSupport, requiresLinking } = await checkLinking(this.resourcePath, source);
70+
if (requiresLinking && !hasLinkerSupport) {
71+
// Cannot link if there is no linker support
72+
this.emitError(
73+
'File requires the Angular linker. "@angular/compiler-cli" version 11.1.0 or greater is needed.',
74+
);
75+
} else {
76+
shouldLink = requiresLinking;
77+
}
78+
shouldProcess ||= shouldLink;
79+
80+
const options: Record<string, unknown> = {
81+
...baseOptions,
82+
...loaderOptions,
83+
};
84+
85+
if (!shouldProcess) {
86+
// Force the current file to be ignored
87+
options.ignore = [() => true];
88+
}
89+
90+
return { custom: { forceES5: !!forceES5, shouldLink }, loader: options };
91+
},
92+
config(configuration, { customOptions }) {
93+
return {
94+
...configuration.options,
95+
presets: [
96+
...(configuration.options.presets || []),
97+
[
98+
require('./presets/application').default,
99+
{
100+
angularLinker: customOptions.shouldLink,
101+
forceES5: customOptions.forceES5,
102+
diagnosticReporter: (type, message) => {
103+
switch (type) {
104+
case 'error':
105+
this.emitError(message);
106+
break;
107+
case 'info':
108+
// Webpack does not currently have an informational diagnostic
109+
case 'warning':
110+
this.emitWarning(message);
111+
break;
112+
}
113+
},
114+
} as import('./presets/application').ApplicationPresetOptions,
115+
],
116+
],
117+
};
118+
},
119+
};
120+
});

packages/angular_devkit/build_angular/src/webpack/configs/common.ts

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -544,35 +544,19 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
544544
sideEffects: true,
545545
},
546546
{
547-
test: /\.m?js$/,
547+
test: /\.[cm]?js$/,
548548
exclude: [/[\/\\](?:core-js|\@babel|tslib|web-animations-js)[\/\\]/, /(ngfactory|ngstyle)\.js$/],
549549
use: [
550-
...(wco.supportES2015
551-
? []
552-
: [
553-
{
554-
loader: require.resolve('babel-loader'),
555-
options: {
556-
babelrc: false,
557-
configFile: false,
558-
compact: false,
559-
cacheCompression: false,
560-
cacheDirectory: findCachePath('babel-webpack'),
561-
cacheIdentifier: JSON.stringify({
562-
buildAngular: require('../../../package.json').version,
563-
}),
564-
sourceType: 'unambiguous',
565-
presets: [
566-
[
567-
require.resolve('../../babel/presets/application'),
568-
{
569-
forceES5: true,
570-
} as import('../../babel/presets/application').ApplicationPresetOptions,
571-
],
572-
],
573-
},
574-
},
575-
]),
550+
{
551+
loader: require.resolve('../../babel/webpack-loader'),
552+
options: {
553+
cacheDirectory: findCachePath('babel-webpack'),
554+
cacheIdentifier: JSON.stringify({
555+
buildAngular: require('../../../package.json').version,
556+
}),
557+
forceES5: !wco.supportES2015,
558+
},
559+
},
576560
...buildOptimizerUseRule,
577561
],
578562
},

0 commit comments

Comments
 (0)