From c3a87a60e0d3cdcae9f4361c2cf21c7ea29bd7de Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:26:15 -0400 Subject: [PATCH] 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 --- .../tools/esbuild/angular/compiler-plugin.ts | 77 ++++++++++++------- .../esbuild/angular/web-worker-transformer.ts | 10 ++- tests/legacy-cli/e2e.bzl | 1 + tests/legacy-cli/e2e/tests/build/worker.ts | 37 ++++++--- 4 files changed, 86 insertions(+), 39 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/angular/compiler-plugin.ts index 894ba5c85fdf..772b16277297 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/angular/compiler-plugin.ts @@ -125,8 +125,8 @@ export function createCompilerPlugin( new Map(); // The stylesheet resources from component stylesheets that will be added to the build results output files - let stylesheetResourceFiles: OutputFile[] = []; - let stylesheetMetafiles: Metafile[]; + let additionalOutputFiles: OutputFile[] = []; + let additionalMetafiles: Metafile[]; // Create new reusable compilation for the appropriate mode based on the `jit` plugin option const compilation: AngularCompilation = pluginOptions.noopTypeScriptCompilation @@ -146,9 +146,9 @@ export function createCompilerPlugin( // Reset debug performance tracking resetCumulativeDurations(); - // Reset stylesheet resource output files - stylesheetResourceFiles = []; - stylesheetMetafiles = []; + // Reset additional output files + additionalOutputFiles = []; + additionalMetafiles = []; // Create Angular compiler host options const hostOptions: AngularHostOptions = { @@ -173,31 +173,50 @@ export function createCompilerPlugin( (result.errors ??= []).push(...errors); } (result.warnings ??= []).push(...warnings); - stylesheetResourceFiles.push(...resourceFiles); + additionalOutputFiles.push(...resourceFiles); if (stylesheetResult.metafile) { - stylesheetMetafiles.push(stylesheetResult.metafile); + additionalMetafiles.push(stylesheetResult.metafile); } return contents; }, processWebWorker(workerFile, containingFile) { - // TODO: Implement bundling of the worker - // This temporarily issues a warning that workers are not yet processed. - (result.warnings ??= []).push({ - text: 'Processing of Web Worker files is not yet implemented.', - location: null, - notes: [ - { - text: `The worker entry point file '${workerFile}' found in '${path.relative( - styleOptions.workspaceRoot, - containingFile, - )}' will not be present in the output.`, - }, - ], + const fullWorkerPath = path.join(path.dirname(containingFile), workerFile); + // The synchronous API must be used due to the TypeScript compilation currently being + // fully synchronous and this process callback being called from within a TypeScript + // transformer. + const workerResult = build.esbuild.buildSync({ + platform: 'browser', + write: false, + bundle: true, + metafile: true, + format: 'esm', + mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'], + sourcemap: pluginOptions.sourcemap, + entryNames: 'worker-[hash]', + entryPoints: [fullWorkerPath], + absWorkingDir: build.initialOptions.absWorkingDir, + outdir: build.initialOptions.outdir, + minifyIdentifiers: build.initialOptions.minifyIdentifiers, + minifySyntax: build.initialOptions.minifySyntax, + minifyWhitespace: build.initialOptions.minifyWhitespace, + target: build.initialOptions.target, }); - // Returning the original file prevents modification to the containing file - return workerFile; + if (workerResult.errors) { + (result.errors ??= []).push(...workerResult.errors); + } + (result.warnings ??= []).push(...workerResult.warnings); + additionalOutputFiles.push(...workerResult.outputFiles); + if (workerResult.metafile) { + additionalMetafiles.push(workerResult.metafile); + } + + // Return bundled worker file entry name to be used in the built output + return path.relative( + build.initialOptions.outdir ?? '', + workerResult.outputFiles[0].path, + ); }, }; @@ -365,20 +384,20 @@ export function createCompilerPlugin( setupJitPluginCallbacks( build, styleOptions, - stylesheetResourceFiles, + additionalOutputFiles, pluginOptions.loadResultCache, ); } build.onEnd((result) => { - // Add any component stylesheet resource files to the output files - if (stylesheetResourceFiles.length) { - result.outputFiles?.push(...stylesheetResourceFiles); + // Add any additional output files to the main output files + if (additionalOutputFiles.length) { + result.outputFiles?.push(...additionalOutputFiles); } - // Combine component stylesheet metafiles with main metafile - if (result.metafile && stylesheetMetafiles.length) { - for (const metafile of stylesheetMetafiles) { + // Combine additional metafiles with main metafile + if (result.metafile && additionalMetafiles.length) { + for (const metafile of additionalMetafiles) { result.metafile.inputs = { ...result.metafile.inputs, ...metafile.inputs }; result.metafile.outputs = { ...result.metafile.outputs, ...metafile.outputs }; } diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/angular/web-worker-transformer.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/angular/web-worker-transformer.ts index 13c78181c921..85ed32c05e31 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/angular/web-worker-transformer.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/angular/web-worker-transformer.ts @@ -97,7 +97,15 @@ export function createWorkerTransformer( workerUrlNode.arguments, ), ), - node.arguments[1], + // Use the second Worker argument (options) if present. + // Otherwise create a default options object for module Workers. + node.arguments[1] ?? + nodeFactory.createObjectLiteralExpression([ + nodeFactory.createPropertyAssignment( + 'type', + nodeFactory.createStringLiteral('module'), + ), + ]), ], node.arguments.hasTrailingComma, ), diff --git a/tests/legacy-cli/e2e.bzl b/tests/legacy-cli/e2e.bzl index 81ccd66a2cac..7e2adedb4d46 100644 --- a/tests/legacy-cli/e2e.bzl +++ b/tests/legacy-cli/e2e.bzl @@ -38,6 +38,7 @@ ESBUILD_TESTS = [ "tests/build/relative-sourcemap.js", "tests/build/styles/**", "tests/build/prerender/**", + "tests/build/worker.js", "tests/commands/add/**", "tests/i18n/**", ] diff --git a/tests/legacy-cli/e2e/tests/build/worker.ts b/tests/legacy-cli/e2e/tests/build/worker.ts index a8dc02e0b4f9..dd44edbf2b10 100644 --- a/tests/legacy-cli/e2e/tests/build/worker.ts +++ b/tests/legacy-cli/e2e/tests/build/worker.ts @@ -9,8 +9,11 @@ import { readdir } from 'fs/promises'; import { expectFileToExist, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs'; import { ng } from '../../utils/process'; +import { getGlobalVariable } from '../../utils/env'; export default async function () { + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + const workerPath = 'src/app/app.worker.ts'; const snippetPath = 'src/app/app.component.ts'; const projectTsConfig = 'tsconfig.json'; @@ -23,14 +26,25 @@ export default async function () { await expectFileToMatch(snippetPath, `new Worker(new URL('./app.worker', import.meta.url)`); await ng('build', '--configuration=development'); - await expectFileToExist('dist/test-project/src_app_app_worker_ts.js'); - await expectFileToMatch('dist/test-project/main.js', 'src_app_app_worker_ts'); + if (useWebpackBuilder) { + await expectFileToExist('dist/test-project/src_app_app_worker_ts.js'); + await expectFileToMatch('dist/test-project/main.js', 'src_app_app_worker_ts'); + } else { + const workerOutputFile = await getWorkerOutputFile(false); + await expectFileToExist(`dist/test-project/${workerOutputFile}`); + await expectFileToMatch('dist/test-project/main.js', workerOutputFile); + } await ng('build', '--output-hashing=none'); - const chunkId = await getWorkerChunkId(); - await expectFileToExist(`dist/test-project/${chunkId}.js`); - await expectFileToMatch('dist/test-project/main.js', chunkId); + const workerOutputFile = await getWorkerOutputFile(useWebpackBuilder); + await expectFileToExist(`dist/test-project/${workerOutputFile}`); + if (useWebpackBuilder) { + // Check Webpack builds for the numeric chunk identifier + await expectFileToMatch('dist/test-project/main.js', workerOutputFile.substring(0, 3)); + } else { + await expectFileToMatch('dist/test-project/main.js', workerOutputFile); + } // console.warn has to be used because chrome only captures warnings and errors by default // https://github.com/angular/protractor/issues/2207 @@ -56,13 +70,18 @@ export default async function () { await ng('e2e'); } -async function getWorkerChunkId(): Promise { +async function getWorkerOutputFile(useWebpackBuilder: boolean): Promise { const files = await readdir('dist/test-project'); - const fileName = files.find((f) => /^\d{3}\.js$/.test(f)); + let fileName; + if (useWebpackBuilder) { + fileName = files.find((f) => /^\d{3}\.js$/.test(f)); + } else { + fileName = files.find((f) => /worker-[\dA-Z]{8}\.js/.test(f)); + } if (!fileName) { - throw new Error('Cannot determine worker chunk Id.'); + throw new Error('Cannot determine worker output file name.'); } - return fileName.substring(0, 3); + return fileName; }