Skip to content

Commit c3a87a6

Browse files
clydinalan-agius4
authored andcommitted
feat(@angular-devkit/build-angular): support basic web worker bundling with esbuild builders
When using the esbuild-based builders (`application`/`browser`), Web Workers that use the supported syntax will now be bundled. The bundling process currently uses an additional synchronous internal esbuild execution. The execution must be synchronous due to the usage within a TypeScript transformer. TypeScript's compilation process is fully synchronous. The bundling itself currently does not provide all the features of the Webpack-based builder. The following limitations are present in the current implementation but will be addressed in upcoming changes: * Worker code is not type-checked * Nested workers are not supported
1 parent 2b7c8c4 commit c3a87a6

File tree

4 files changed

+86
-39
lines changed

4 files changed

+86
-39
lines changed

packages/angular_devkit/build_angular/src/tools/esbuild/angular/compiler-plugin.ts

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ export function createCompilerPlugin(
125125
new Map<string, string | Uint8Array>();
126126

127127
// The stylesheet resources from component stylesheets that will be added to the build results output files
128-
let stylesheetResourceFiles: OutputFile[] = [];
129-
let stylesheetMetafiles: Metafile[];
128+
let additionalOutputFiles: OutputFile[] = [];
129+
let additionalMetafiles: Metafile[];
130130

131131
// Create new reusable compilation for the appropriate mode based on the `jit` plugin option
132132
const compilation: AngularCompilation = pluginOptions.noopTypeScriptCompilation
@@ -146,9 +146,9 @@ export function createCompilerPlugin(
146146
// Reset debug performance tracking
147147
resetCumulativeDurations();
148148

149-
// Reset stylesheet resource output files
150-
stylesheetResourceFiles = [];
151-
stylesheetMetafiles = [];
149+
// Reset additional output files
150+
additionalOutputFiles = [];
151+
additionalMetafiles = [];
152152

153153
// Create Angular compiler host options
154154
const hostOptions: AngularHostOptions = {
@@ -173,31 +173,50 @@ export function createCompilerPlugin(
173173
(result.errors ??= []).push(...errors);
174174
}
175175
(result.warnings ??= []).push(...warnings);
176-
stylesheetResourceFiles.push(...resourceFiles);
176+
additionalOutputFiles.push(...resourceFiles);
177177
if (stylesheetResult.metafile) {
178-
stylesheetMetafiles.push(stylesheetResult.metafile);
178+
additionalMetafiles.push(stylesheetResult.metafile);
179179
}
180180

181181
return contents;
182182
},
183183
processWebWorker(workerFile, containingFile) {
184-
// TODO: Implement bundling of the worker
185-
// This temporarily issues a warning that workers are not yet processed.
186-
(result.warnings ??= []).push({
187-
text: 'Processing of Web Worker files is not yet implemented.',
188-
location: null,
189-
notes: [
190-
{
191-
text: `The worker entry point file '${workerFile}' found in '${path.relative(
192-
styleOptions.workspaceRoot,
193-
containingFile,
194-
)}' will not be present in the output.`,
195-
},
196-
],
184+
const fullWorkerPath = path.join(path.dirname(containingFile), workerFile);
185+
// The synchronous API must be used due to the TypeScript compilation currently being
186+
// fully synchronous and this process callback being called from within a TypeScript
187+
// transformer.
188+
const workerResult = build.esbuild.buildSync({
189+
platform: 'browser',
190+
write: false,
191+
bundle: true,
192+
metafile: true,
193+
format: 'esm',
194+
mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'],
195+
sourcemap: pluginOptions.sourcemap,
196+
entryNames: 'worker-[hash]',
197+
entryPoints: [fullWorkerPath],
198+
absWorkingDir: build.initialOptions.absWorkingDir,
199+
outdir: build.initialOptions.outdir,
200+
minifyIdentifiers: build.initialOptions.minifyIdentifiers,
201+
minifySyntax: build.initialOptions.minifySyntax,
202+
minifyWhitespace: build.initialOptions.minifyWhitespace,
203+
target: build.initialOptions.target,
197204
});
198205

199-
// Returning the original file prevents modification to the containing file
200-
return workerFile;
206+
if (workerResult.errors) {
207+
(result.errors ??= []).push(...workerResult.errors);
208+
}
209+
(result.warnings ??= []).push(...workerResult.warnings);
210+
additionalOutputFiles.push(...workerResult.outputFiles);
211+
if (workerResult.metafile) {
212+
additionalMetafiles.push(workerResult.metafile);
213+
}
214+
215+
// Return bundled worker file entry name to be used in the built output
216+
return path.relative(
217+
build.initialOptions.outdir ?? '',
218+
workerResult.outputFiles[0].path,
219+
);
201220
},
202221
};
203222

@@ -365,20 +384,20 @@ export function createCompilerPlugin(
365384
setupJitPluginCallbacks(
366385
build,
367386
styleOptions,
368-
stylesheetResourceFiles,
387+
additionalOutputFiles,
369388
pluginOptions.loadResultCache,
370389
);
371390
}
372391

373392
build.onEnd((result) => {
374-
// Add any component stylesheet resource files to the output files
375-
if (stylesheetResourceFiles.length) {
376-
result.outputFiles?.push(...stylesheetResourceFiles);
393+
// Add any additional output files to the main output files
394+
if (additionalOutputFiles.length) {
395+
result.outputFiles?.push(...additionalOutputFiles);
377396
}
378397

379-
// Combine component stylesheet metafiles with main metafile
380-
if (result.metafile && stylesheetMetafiles.length) {
381-
for (const metafile of stylesheetMetafiles) {
398+
// Combine additional metafiles with main metafile
399+
if (result.metafile && additionalMetafiles.length) {
400+
for (const metafile of additionalMetafiles) {
382401
result.metafile.inputs = { ...result.metafile.inputs, ...metafile.inputs };
383402
result.metafile.outputs = { ...result.metafile.outputs, ...metafile.outputs };
384403
}

packages/angular_devkit/build_angular/src/tools/esbuild/angular/web-worker-transformer.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,15 @@ export function createWorkerTransformer(
9797
workerUrlNode.arguments,
9898
),
9999
),
100-
node.arguments[1],
100+
// Use the second Worker argument (options) if present.
101+
// Otherwise create a default options object for module Workers.
102+
node.arguments[1] ??
103+
nodeFactory.createObjectLiteralExpression([
104+
nodeFactory.createPropertyAssignment(
105+
'type',
106+
nodeFactory.createStringLiteral('module'),
107+
),
108+
]),
101109
],
102110
node.arguments.hasTrailingComma,
103111
),

tests/legacy-cli/e2e.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ ESBUILD_TESTS = [
3838
"tests/build/relative-sourcemap.js",
3939
"tests/build/styles/**",
4040
"tests/build/prerender/**",
41+
"tests/build/worker.js",
4142
"tests/commands/add/**",
4243
"tests/i18n/**",
4344
]

tests/legacy-cli/e2e/tests/build/worker.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
import { readdir } from 'fs/promises';
1010
import { expectFileToExist, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs';
1111
import { ng } from '../../utils/process';
12+
import { getGlobalVariable } from '../../utils/env';
1213

1314
export default async function () {
15+
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
16+
1417
const workerPath = 'src/app/app.worker.ts';
1518
const snippetPath = 'src/app/app.component.ts';
1619
const projectTsConfig = 'tsconfig.json';
@@ -23,14 +26,25 @@ export default async function () {
2326
await expectFileToMatch(snippetPath, `new Worker(new URL('./app.worker', import.meta.url)`);
2427

2528
await ng('build', '--configuration=development');
26-
await expectFileToExist('dist/test-project/src_app_app_worker_ts.js');
27-
await expectFileToMatch('dist/test-project/main.js', 'src_app_app_worker_ts');
29+
if (useWebpackBuilder) {
30+
await expectFileToExist('dist/test-project/src_app_app_worker_ts.js');
31+
await expectFileToMatch('dist/test-project/main.js', 'src_app_app_worker_ts');
32+
} else {
33+
const workerOutputFile = await getWorkerOutputFile(false);
34+
await expectFileToExist(`dist/test-project/${workerOutputFile}`);
35+
await expectFileToMatch('dist/test-project/main.js', workerOutputFile);
36+
}
2837

2938
await ng('build', '--output-hashing=none');
3039

31-
const chunkId = await getWorkerChunkId();
32-
await expectFileToExist(`dist/test-project/${chunkId}.js`);
33-
await expectFileToMatch('dist/test-project/main.js', chunkId);
40+
const workerOutputFile = await getWorkerOutputFile(useWebpackBuilder);
41+
await expectFileToExist(`dist/test-project/${workerOutputFile}`);
42+
if (useWebpackBuilder) {
43+
// Check Webpack builds for the numeric chunk identifier
44+
await expectFileToMatch('dist/test-project/main.js', workerOutputFile.substring(0, 3));
45+
} else {
46+
await expectFileToMatch('dist/test-project/main.js', workerOutputFile);
47+
}
3448

3549
// console.warn has to be used because chrome only captures warnings and errors by default
3650
// https://github.com/angular/protractor/issues/2207
@@ -56,13 +70,18 @@ export default async function () {
5670
await ng('e2e');
5771
}
5872

59-
async function getWorkerChunkId(): Promise<string> {
73+
async function getWorkerOutputFile(useWebpackBuilder: boolean): Promise<string> {
6074
const files = await readdir('dist/test-project');
61-
const fileName = files.find((f) => /^\d{3}\.js$/.test(f));
75+
let fileName;
76+
if (useWebpackBuilder) {
77+
fileName = files.find((f) => /^\d{3}\.js$/.test(f));
78+
} else {
79+
fileName = files.find((f) => /worker-[\dA-Z]{8}\.js/.test(f));
80+
}
6281

6382
if (!fileName) {
64-
throw new Error('Cannot determine worker chunk Id.');
83+
throw new Error('Cannot determine worker output file name.');
6584
}
6685

67-
return fileName.substring(0, 3);
86+
return fileName;
6887
}

0 commit comments

Comments
 (0)