-
Notifications
You must be signed in to change notification settings - Fork 11.9k
Expand file tree
/
Copy pathi18n-inliner-worker.ts
More file actions
230 lines (199 loc) · 7.56 KB
/
i18n-inliner-worker.ts
File metadata and controls
230 lines (199 loc) · 7.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import { PluginObj, parseSync, transformFromAstAsync, types } from '@babel/core';
import remapping, { SourceMapInput } from '@jridgewell/remapping';
import assert from 'node:assert';
import { workerData } from 'node:worker_threads';
import { assertIsError } from '../../utils/error';
import { loadEsmModule } from '../../utils/load-esm';
/**
* The options passed to the inliner for each file request
*/
interface InlineFileRequest {
/**
* The filename that should be processed. The data for the file is provided to the Worker
* during Worker initialization.
*/
filename: string;
/**
* The locale specifier that should be used during the inlining process of the file.
*/
locale: string;
/**
* The translation messages for the locale that should be used during the inlining process of the file.
*/
translation?: Record<string, unknown>;
}
/**
* The options passed to the inliner for each code request
*/
interface InlineCodeRequest {
/**
* The code that should be processed.
*/
code: string;
/**
* The filename to use in error and warning messages for the provided code.
*/
filename: string;
/**
* The locale specifier that should be used during the inlining process of the file.
*/
locale: string;
/**
* The translation messages for the locale that should be used during the inlining process of the file.
*/
translation?: Record<string, unknown>;
}
// Extract the application files and common options used for inline requests from the Worker context
// TODO: Evaluate overall performance difference of passing translations here as well
const { files, missingTranslation, shouldOptimize } = (workerData || {}) as {
files: ReadonlyMap<string, Blob>;
missingTranslation: 'error' | 'warning' | 'ignore';
shouldOptimize: boolean;
};
/**
* Inlines the provided locale and translation into a JavaScript file that contains `$localize` usage.
* This function is the main entry for the Worker's action that is called by the worker pool.
*
* @param request An InlineRequest object representing the options for inlining
* @returns An object containing the inlined file and optional map content.
*/
export default async function inlineFile(request: InlineFileRequest) {
const data = files.get(request.filename);
assert(data !== undefined, `Invalid inline request for file '${request.filename}'.`);
const code = await data.text();
const map = await files.get(request.filename + '.map')?.text();
const result = await transformWithBabel(
code,
map && (JSON.parse(map) as SourceMapInput),
request,
);
return {
file: request.filename,
code: result.code,
map: result.map,
messages: result.diagnostics.messages,
};
}
/**
* Inlines the provided locale and translation into JavaScript code that contains `$localize` usage.
* This function is a secondary entry primarily for use with component HMR update modules.
*
* @param request An InlineRequest object representing the options for inlining
* @returns An object containing the inlined code.
*/
export async function inlineCode(request: InlineCodeRequest) {
const result = await transformWithBabel(request.code, undefined, request);
return {
output: result.code,
messages: result.diagnostics.messages,
};
}
/**
* A Type representing the localize tools module.
*/
type LocalizeUtilityModule = typeof import('@angular/localize/tools');
/**
* Cached instance of the `@angular/localize/tools` module.
* This is used to remove the need to repeatedly import the module per file translation.
*/
let localizeToolsModule: LocalizeUtilityModule | undefined;
/**
* Attempts to load the `@angular/localize/tools` module containing the functionality to
* perform the file translations.
* This module must be dynamically loaded as it is an ESM module and this file is CommonJS.
*/
async function loadLocalizeTools(): Promise<LocalizeUtilityModule> {
// Load ESM `@angular/localize/tools` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
localizeToolsModule ??= await loadEsmModule<LocalizeUtilityModule>('@angular/localize/tools');
return localizeToolsModule;
}
/**
* Creates the needed Babel plugins to inline a given locale and translation for a JavaScript file.
* @param locale A string containing the locale specifier to use.
* @param translation A object record containing locale specific messages to use.
* @returns An array of Babel plugins.
*/
async function createI18nPlugins(locale: string, translation: Record<string, unknown> | undefined) {
const { Diagnostics, makeEs2015TranslatePlugin } = await loadLocalizeTools();
const plugins: PluginObj[] = [];
const diagnostics = new Diagnostics();
plugins.push(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
makeEs2015TranslatePlugin(diagnostics, (translation || {}) as any, {
missingTranslation: translation === undefined ? 'ignore' : missingTranslation,
}),
);
// Create a plugin to replace the locale specifier constant inject by the build system with the actual specifier
plugins.push({
visitor: {
StringLiteral(path) {
if (path.node.value === '___NG_LOCALE_INSERT___') {
path.replaceWith(types.stringLiteral(locale));
}
},
},
});
return { diagnostics, plugins };
}
/**
* Transforms a JavaScript file using Babel to inline the request locale and translation.
* @param code A string containing the JavaScript code to transform.
* @param map A sourcemap object for the provided JavaScript code.
* @param options The inline request options to use.
* @returns An object containing the code, map, and diagnostics from the transformation.
*/
async function transformWithBabel(
code: string,
map: SourceMapInput | undefined,
options: InlineFileRequest,
) {
let ast;
try {
ast = parseSync(code, {
babelrc: false,
configFile: false,
sourceType: 'unambiguous',
filename: options.filename,
});
} catch (error) {
assertIsError(error);
// Make the error more readable.
// Same errors will contain the full content of the file as the error message
// Which makes it hard to find the actual error message.
const index = error.message.indexOf(')\n');
const msg = index !== -1 ? error.message.slice(0, index + 1) : error.message;
throw new Error(`${msg}\nAn error occurred inlining file "${options.filename}"`);
}
if (!ast) {
throw new Error(`Unknown error occurred inlining file "${options.filename}"`);
}
const { diagnostics, plugins } = await createI18nPlugins(options.locale, options.translation);
const transformResult = await transformFromAstAsync(ast, code, {
filename: options.filename,
// false is a valid value but not included in the type definition
inputSourceMap: false as unknown as undefined,
sourceMaps: !!map,
compact: shouldOptimize,
configFile: false,
babelrc: false,
browserslistConfigFile: false,
plugins,
});
if (!transformResult || !transformResult.code) {
throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`);
}
let outputMap;
if (map && transformResult.map) {
outputMap = remapping([transformResult.map as SourceMapInput, map], () => null);
}
return { code: transformResult.code, map: outputMap && JSON.stringify(outputMap), diagnostics };
}