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; }