Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): support basic web worker bundlin…
Browse files Browse the repository at this point in the history
…g 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
  • Loading branch information
clydin authored and alan-agius4 committed Sep 22, 2023
1 parent 2b7c8c4 commit c3a87a6
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ export function createCompilerPlugin(
new Map<string, string | Uint8Array>();

// 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
Expand All @@ -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 = {
Expand All @@ -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,
);
},
};

Expand Down Expand Up @@ -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 };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down
1 change: 1 addition & 0 deletions tests/legacy-cli/e2e.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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/**",
]
Expand Down
37 changes: 28 additions & 9 deletions tests/legacy-cli/e2e/tests/build/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand All @@ -56,13 +70,18 @@ export default async function () {
await ng('e2e');
}

async function getWorkerChunkId(): Promise<string> {
async function getWorkerOutputFile(useWebpackBuilder: boolean): Promise<string> {
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;
}

0 comments on commit c3a87a6

Please sign in to comment.