Skip to content

Commit 9a2d82f

Browse files
committed
refactor(@angular-devkit/build-angular): use custom babel loader for i18n dev-server support
The custom babel loader allows files to be conditionally processed by the i18n inlining transforms based on both file path and content. By allowing content based checks, the entire parse/transform/print process can be skipped for files that do not contain localizations. (cherry picked from commit e1b3ee6)
1 parent da59254 commit 9a2d82f

File tree

2 files changed

+50
-65
lines changed

2 files changed

+50
-65
lines changed

packages/angular_devkit/build_angular/src/babel/webpack-loader.ts

+40-23
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
*/
88
import { custom } from 'babel-loader';
99
import { ScriptTarget } from 'typescript';
10+
import { ApplicationPresetOptions } from './presets/application';
1011

1112
interface AngularCustomOptions {
1213
forceAsyncTransformation: boolean;
1314
forceES5: boolean;
1415
shouldLink: boolean;
16+
i18n: ApplicationPresetOptions['i18n'];
1517
}
1618

1719
/**
@@ -65,56 +67,70 @@ export default custom<AngularCustomOptions>(() => {
6567
});
6668

6769
return {
68-
async customOptions({ scriptTarget, ...loaderOptions }, { source }) {
70+
async customOptions({ i18n, scriptTarget, ...rawOptions }, { source }) {
6971
// Must process file if plugins are added
70-
let shouldProcess = Array.isArray(loaderOptions.plugins) && loaderOptions.plugins.length > 0;
72+
let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0;
73+
74+
const customOptions: AngularCustomOptions = {
75+
forceAsyncTransformation: false,
76+
forceES5: false,
77+
shouldLink: false,
78+
i18n: undefined,
79+
};
7180

7281
// Analyze file for linking
73-
let shouldLink = false;
7482
const { hasLinkerSupport, requiresLinking } = await checkLinking(this.resourcePath, source);
7583
if (requiresLinking && !hasLinkerSupport) {
7684
// Cannot link if there is no linker support
7785
this.emitError(
7886
'File requires the Angular linker. "@angular/compiler-cli" version 11.1.0 or greater is needed.',
7987
);
8088
} else {
81-
shouldLink = requiresLinking;
89+
customOptions.shouldLink = requiresLinking;
8290
}
83-
shouldProcess ||= shouldLink;
91+
shouldProcess ||= customOptions.shouldLink;
8492

8593
// Analyze for ES target processing
86-
let forceES5 = false;
87-
let forceAsyncTransformation = false;
88-
const esTarget = scriptTarget as ScriptTarget;
89-
if (esTarget < ScriptTarget.ES2015) {
90-
// TypeScript files will have already been downlevelled
91-
forceES5 = !/\.tsx?$/.test(this.resourcePath);
92-
} else if (esTarget >= ScriptTarget.ES2017) {
93-
forceAsyncTransformation = source.includes('async');
94+
const esTarget = scriptTarget as ScriptTarget | undefined;
95+
if (esTarget !== undefined) {
96+
if (esTarget < ScriptTarget.ES2015) {
97+
// TypeScript files will have already been downlevelled
98+
customOptions.forceES5 = !/\.tsx?$/.test(this.resourcePath);
99+
} else if (esTarget >= ScriptTarget.ES2017) {
100+
customOptions.forceAsyncTransformation = source.includes('async');
101+
}
102+
shouldProcess ||= customOptions.forceAsyncTransformation || customOptions.forceES5;
103+
}
104+
105+
// Analyze for i18n inlining
106+
if (
107+
i18n &&
108+
!/[\\\/]@angular[\\\/](?:compiler|localize)/.test(this.resourcePath) &&
109+
source.includes('$localize')
110+
) {
111+
customOptions.i18n = i18n as ApplicationPresetOptions['i18n'];
112+
shouldProcess = true;
94113
}
95-
shouldProcess ||= forceAsyncTransformation || forceES5;
96114

97115
// Add provided loader options to default base options
98-
const options: Record<string, unknown> = {
116+
const loaderOptions: Record<string, unknown> = {
99117
...baseOptions,
100-
...loaderOptions,
118+
...rawOptions,
101119
cacheIdentifier: JSON.stringify({
102120
buildAngular: require('../../package.json').version,
103-
forceAsyncTransformation,
104-
forceES5,
105-
shouldLink,
121+
customOptions,
106122
baseOptions,
107-
loaderOptions,
123+
rawOptions,
108124
}),
109125
};
110126

111127
// Skip babel processing if no actions are needed
112128
if (!shouldProcess) {
113129
// Force the current file to be ignored
114-
options.ignore = [() => true];
130+
loaderOptions.ignore = [() => true];
115131
}
116132

117-
return { custom: { forceAsyncTransformation, forceES5, shouldLink }, loader: options };
133+
return { custom: customOptions, loader: loaderOptions };
118134
},
119135
config(configuration, { customOptions }) {
120136
return {
@@ -127,6 +143,7 @@ export default custom<AngularCustomOptions>(() => {
127143
angularLinker: customOptions.shouldLink,
128144
forceES5: customOptions.forceES5,
129145
forceAsyncTransformation: customOptions.forceAsyncTransformation,
146+
i18n: customOptions.i18n,
130147
diagnosticReporter: (type, message) => {
131148
switch (type) {
132149
case 'error':
@@ -139,7 +156,7 @@ export default custom<AngularCustomOptions>(() => {
139156
break;
140157
}
141158
},
142-
} as import('./presets/application').ApplicationPresetOptions,
159+
} as ApplicationPresetOptions,
143160
],
144161
],
145162
};

packages/angular_devkit/build_angular/src/dev-server/index.ts

+10-42
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,6 @@ async function setupLocalize(
354354
webpackConfig: webpack.Configuration,
355355
) {
356356
const localeDescription = i18n.locales[locale];
357-
const i18nDiagnostics: { type: string, message: string }[] = [];
358357

359358
// Modify main entrypoint to include locale data
360359
if (
@@ -378,37 +377,25 @@ async function setupLocalize(
378377
translation = {};
379378
}
380379

380+
const i18nLoaderOptions = {
381+
locale,
382+
missingTranslationBehavior,
383+
translation: i18n.shouldInline ? translation : undefined,
384+
};
385+
381386
const i18nRule: webpack.RuleSetRule = {
382-
test: /\.(?:m?js|ts)$/,
387+
test: /\.(?:[cm]?js|ts)$/,
383388
enforce: 'post',
384389
use: [
385390
{
386-
loader: require.resolve('babel-loader'),
391+
loader: require.resolve('../babel/webpack-loader'),
387392
options: {
388-
babelrc: false,
389-
configFile: false,
390-
compact: false,
391-
cacheCompression: false,
392-
cacheDirectory: findCachePath('babel-loader'),
393+
cacheDirectory: findCachePath('babel-dev-server-i18n'),
393394
cacheIdentifier: JSON.stringify({
394-
buildAngular: require('../../package.json').version,
395395
locale,
396396
translationIntegrity: localeDescription?.files.map((file) => file.integrity),
397397
}),
398-
sourceType: 'unambiguous',
399-
presets: [
400-
[
401-
require.resolve('../babel/presets/application'),
402-
{
403-
i18n: {
404-
locale,
405-
translation: i18n.shouldInline ? translation : undefined,
406-
missingTranslationBehavior,
407-
},
408-
diagnosticReporter: (type, message) => i18nDiagnostics.push({ type, message }),
409-
} as import('../babel/presets/application').ApplicationPresetOptions,
410-
],
411-
],
398+
i18n: i18nLoaderOptions,
412399
},
413400
},
414401
],
@@ -423,25 +410,6 @@ async function setupLocalize(
423410
}
424411

425412
rules.push(i18nRule);
426-
427-
// Add a plugin to inject the i18n diagnostics
428-
// tslint:disable-next-line: no-non-null-assertion
429-
webpackConfig.plugins!.push({
430-
apply: (compiler: webpack.Compiler) => {
431-
compiler.hooks.thisCompilation.tap('build-angular', compilation => {
432-
compilation.hooks.finishModules.tap('build-angular', () => {
433-
for (const diagnostic of i18nDiagnostics) {
434-
if (diagnostic.type === 'error') {
435-
addError(compilation, diagnostic.message);
436-
} else {
437-
addWarning(compilation, diagnostic.message);
438-
}
439-
}
440-
i18nDiagnostics.length = 0;
441-
});
442-
});
443-
},
444-
});
445413
}
446414

447415
export default createBuilder<DevServerBuilderOptions, DevServerBuilderOutput>(serveWebpackBrowser);

0 commit comments

Comments
 (0)