Skip to content

Commit 8bce80b

Browse files
clydinalan-agius4
authored andcommitted
feat(@angular-devkit/build-angular): initial support for application Web Worker discovery with esbuild
When using the esbuild-based builders (application/browser-esbuild), Web Workers following the previously supported syntax as used in the Webpack-based builder will now be discovered. The worker entry points are not yet bundled or otherwise processed. Currently, a warning will be issued to notify that the worker will not be present in the built output. Additional upcoming changes will add the processing and bundling support for the workers. Web Worker syntax example: `new Worker(new URL('./my-worker-file', import.meta.url), { type: 'module' });`
1 parent 4e89c3c commit 8bce80b

File tree

5 files changed

+158
-3
lines changed

5 files changed

+158
-3
lines changed

Diff for: packages/angular_devkit/build_angular/src/tools/esbuild/angular/angular-host.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface AngularHostOptions {
2121
containingFile: string,
2222
stylesheetFile?: string,
2323
): Promise<string | null>;
24+
processWebWorker(workerFile: string, containingFile: string): string;
2425
}
2526

2627
// Temporary deep import for host augmentation support.

Diff for: packages/angular_devkit/build_angular/src/tools/esbuild/angular/compilation/aot-compilation.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
createAngularCompilerHost,
1616
ensureSourceFileVersions,
1717
} from '../angular-host';
18+
import { createWorkerTransformer } from '../web-worker-transformer';
1819
import { AngularCompilation, EmitFileResult } from './angular-compilation';
1920

2021
// Temporary deep import for transformer support
@@ -28,6 +29,7 @@ class AngularCompilationState {
2829
public readonly typeScriptProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram,
2930
public readonly affectedFiles: ReadonlySet<ts.SourceFile>,
3031
public readonly templateDiagnosticsOptimization: ng.OptimizeFor,
32+
public readonly webWorkerTransform: ts.TransformerFactory<ts.SourceFile>,
3133
public readonly diagnosticCache = new WeakMap<ts.SourceFile, ts.Diagnostic[]>(),
3234
) {}
3335

@@ -97,6 +99,7 @@ export class AotCompilation extends AngularCompilation {
9799
typeScriptProgram,
98100
affectedFiles,
99101
affectedFiles.size === 1 ? OptimizeFor.SingleFile : OptimizeFor.WholeProgram,
102+
createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)),
100103
this.#state?.diagnosticCache,
101104
);
102105

@@ -172,7 +175,7 @@ export class AotCompilation extends AngularCompilation {
172175

173176
emitAffectedFiles(): Iterable<EmitFileResult> {
174177
assert(this.#state, 'Angular compilation must be initialized prior to emitting files.');
175-
const { angularCompiler, compilerHost, typeScriptProgram } = this.#state;
178+
const { angularCompiler, compilerHost, typeScriptProgram, webWorkerTransform } = this.#state;
176179
const buildInfoFilename =
177180
typeScriptProgram.getCompilerOptions().tsBuildInfoFile ?? '.tsbuildinfo';
178181

@@ -195,7 +198,10 @@ export class AotCompilation extends AngularCompilation {
195198
emittedFiles.set(sourceFile, { filename: sourceFile.fileName, contents });
196199
};
197200
const transformers = mergeTransformers(angularCompiler.prepareEmit().transformers, {
198-
before: [replaceBootstrap(() => typeScriptProgram.getProgram().getTypeChecker())],
201+
before: [
202+
replaceBootstrap(() => typeScriptProgram.getProgram().getTypeChecker()),
203+
webWorkerTransform,
204+
],
199205
});
200206

201207
// TypeScript will loop until there are no more affected files in the program

Diff for: packages/angular_devkit/build_angular/src/tools/esbuild/angular/compilation/jit-compilation.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ts from 'typescript';
1212
import { profileSync } from '../../profiling';
1313
import { AngularHostOptions, createAngularCompilerHost } from '../angular-host';
1414
import { createJitResourceTransformer } from '../jit-resource-transformer';
15+
import { createWorkerTransformer } from '../web-worker-transformer';
1516
import { AngularCompilation, EmitFileResult } from './angular-compilation';
1617

1718
class JitCompilationState {
@@ -20,6 +21,7 @@ class JitCompilationState {
2021
public readonly typeScriptProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram,
2122
public readonly constructorParametersDownlevelTransform: ts.TransformerFactory<ts.SourceFile>,
2223
public readonly replaceResourcesTransform: ts.TransformerFactory<ts.SourceFile>,
24+
public readonly webWorkerTransform: ts.TransformerFactory<ts.SourceFile>,
2325
) {}
2426
}
2527

@@ -70,6 +72,7 @@ export class JitCompilation extends AngularCompilation {
7072
typeScriptProgram,
7173
constructorParametersDownlevelTransform(typeScriptProgram.getProgram()),
7274
createJitResourceTransformer(() => typeScriptProgram.getProgram().getTypeChecker()),
75+
createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)),
7376
);
7477

7578
const referencedFiles = typeScriptProgram
@@ -100,6 +103,7 @@ export class JitCompilation extends AngularCompilation {
100103
typeScriptProgram,
101104
constructorParametersDownlevelTransform,
102105
replaceResourcesTransform,
106+
webWorkerTransform,
103107
} = this.#state;
104108
const buildInfoFilename =
105109
typeScriptProgram.getCompilerOptions().tsBuildInfoFile ?? '.tsbuildinfo';
@@ -118,7 +122,11 @@ export class JitCompilation extends AngularCompilation {
118122
emittedFiles.push({ filename: sourceFiles[0].fileName, contents });
119123
};
120124
const transformers = {
121-
before: [replaceResourcesTransform, constructorParametersDownlevelTransform],
125+
before: [
126+
replaceResourcesTransform,
127+
constructorParametersDownlevelTransform,
128+
webWorkerTransform,
129+
],
122130
};
123131

124132
// TypeScript will loop until there are no more affected files in the program

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

+19
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,25 @@ export function createCompilerPlugin(
180180

181181
return contents;
182182
},
183+
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+
],
197+
});
198+
199+
// Returning the original file prevents modification to the containing file
200+
return workerFile;
201+
},
183202
};
184203

185204
// Initialize the Angular compilation for the current build.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import ts from 'typescript';
10+
11+
/**
12+
* Creates a TypeScript Transformer to process Worker and SharedWorker entry points and transform
13+
* the URL instances to reference the built and bundled worker code. This uses a callback process
14+
* similar to the component stylesheets to allow the main esbuild plugin to process files as needed.
15+
* Unsupported worker expressions will be left in their origin form.
16+
* @param getTypeChecker A function that returns a TypeScript TypeChecker instance for the program.
17+
* @returns A TypeScript transformer factory.
18+
*/
19+
export function createWorkerTransformer(
20+
fileProcessor: (file: string, importer: string) => string,
21+
): ts.TransformerFactory<ts.SourceFile> {
22+
return (context: ts.TransformationContext) => {
23+
const nodeFactory = context.factory;
24+
25+
const visitNode: ts.Visitor = (node: ts.Node) => {
26+
// Check if the node is a valid new expression for a Worker or SharedWorker
27+
// TODO: Add global scope check
28+
if (
29+
!ts.isNewExpression(node) ||
30+
!ts.isIdentifier(node.expression) ||
31+
(node.expression.text !== 'Worker' && node.expression.text !== 'SharedWorker')
32+
) {
33+
// Visit child nodes of non-Worker expressions
34+
return ts.visitEachChild(node, visitNode, context);
35+
}
36+
37+
// Worker should have atleast one argument but not more than two
38+
if (!node.arguments || node.arguments.length < 1 || node.arguments.length > 2) {
39+
return node;
40+
}
41+
42+
// First argument must be a new URL expression
43+
const workerUrlNode = node.arguments[0];
44+
// TODO: Add global scope check
45+
if (
46+
!ts.isNewExpression(workerUrlNode) ||
47+
!ts.isIdentifier(workerUrlNode.expression) ||
48+
workerUrlNode.expression.text !== 'URL'
49+
) {
50+
return node;
51+
}
52+
53+
// URL must have 2 arguments
54+
if (!workerUrlNode.arguments || workerUrlNode.arguments.length !== 2) {
55+
return node;
56+
}
57+
58+
// URL arguments must be a string and then `import.meta.url`
59+
if (
60+
!ts.isStringLiteralLike(workerUrlNode.arguments[0]) ||
61+
!ts.isPropertyAccessExpression(workerUrlNode.arguments[1]) ||
62+
!ts.isMetaProperty(workerUrlNode.arguments[1].expression) ||
63+
workerUrlNode.arguments[1].name.text !== 'url'
64+
) {
65+
return node;
66+
}
67+
68+
const filePath = workerUrlNode.arguments[0].text;
69+
const importer = node.getSourceFile().fileName;
70+
71+
// Process the file
72+
const replacementPath = fileProcessor(filePath, importer);
73+
74+
// Update if the path changed
75+
if (replacementPath !== filePath) {
76+
return nodeFactory.updateNewExpression(
77+
node,
78+
node.expression,
79+
node.typeArguments,
80+
// Update Worker arguments
81+
ts.setTextRange(
82+
nodeFactory.createNodeArray(
83+
[
84+
nodeFactory.updateNewExpression(
85+
workerUrlNode,
86+
workerUrlNode.expression,
87+
workerUrlNode.typeArguments,
88+
// Update URL arguments
89+
ts.setTextRange(
90+
nodeFactory.createNodeArray(
91+
[
92+
nodeFactory.createStringLiteral(replacementPath),
93+
workerUrlNode.arguments[1],
94+
],
95+
workerUrlNode.arguments.hasTrailingComma,
96+
),
97+
workerUrlNode.arguments,
98+
),
99+
),
100+
node.arguments[1],
101+
],
102+
node.arguments.hasTrailingComma,
103+
),
104+
node.arguments,
105+
),
106+
);
107+
} else {
108+
return node;
109+
}
110+
};
111+
112+
return (sourceFile) => {
113+
// Skip transformer if there are no Workers
114+
if (!sourceFile.text.includes('Worker')) {
115+
return sourceFile;
116+
}
117+
118+
return ts.visitEachChild(sourceFile, visitNode, context);
119+
};
120+
};
121+
}

0 commit comments

Comments
 (0)