Skip to content

Commit ce0a753

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.
1 parent 4cfdb4c commit ce0a753

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

Lines changed: 40 additions & 23 deletions
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

Lines changed: 10 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,6 @@ async function setupLocalize(
355355
webpackConfig: webpack.Configuration,
356356
) {
357357
const localeDescription = i18n.locales[locale];
358-
const i18nDiagnostics: { type: string, message: string }[] = [];
359358

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

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

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

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

0 commit comments

Comments
 (0)