Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): initial support for application …
Browse files Browse the repository at this point in the history
…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' });`
  • Loading branch information
clydin authored and alan-agius4 committed Sep 21, 2023
1 parent 4e89c3c commit 8bce80b
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface AngularHostOptions {
containingFile: string,
stylesheetFile?: string,
): Promise<string | null>;
processWebWorker(workerFile: string, containingFile: string): string;
}

// Temporary deep import for host augmentation support.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
createAngularCompilerHost,
ensureSourceFileVersions,
} from '../angular-host';
import { createWorkerTransformer } from '../web-worker-transformer';
import { AngularCompilation, EmitFileResult } from './angular-compilation';

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

Expand Down Expand Up @@ -97,6 +99,7 @@ export class AotCompilation extends AngularCompilation {
typeScriptProgram,
affectedFiles,
affectedFiles.size === 1 ? OptimizeFor.SingleFile : OptimizeFor.WholeProgram,
createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)),
this.#state?.diagnosticCache,
);

Expand Down Expand Up @@ -172,7 +175,7 @@ export class AotCompilation extends AngularCompilation {

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

Expand All @@ -195,7 +198,10 @@ export class AotCompilation extends AngularCompilation {
emittedFiles.set(sourceFile, { filename: sourceFile.fileName, contents });
};
const transformers = mergeTransformers(angularCompiler.prepareEmit().transformers, {
before: [replaceBootstrap(() => typeScriptProgram.getProgram().getTypeChecker())],
before: [
replaceBootstrap(() => typeScriptProgram.getProgram().getTypeChecker()),
webWorkerTransform,
],
});

// TypeScript will loop until there are no more affected files in the program
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ts from 'typescript';
import { profileSync } from '../../profiling';
import { AngularHostOptions, createAngularCompilerHost } from '../angular-host';
import { createJitResourceTransformer } from '../jit-resource-transformer';
import { createWorkerTransformer } from '../web-worker-transformer';
import { AngularCompilation, EmitFileResult } from './angular-compilation';

class JitCompilationState {
Expand All @@ -20,6 +21,7 @@ class JitCompilationState {
public readonly typeScriptProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram,
public readonly constructorParametersDownlevelTransform: ts.TransformerFactory<ts.SourceFile>,
public readonly replaceResourcesTransform: ts.TransformerFactory<ts.SourceFile>,
public readonly webWorkerTransform: ts.TransformerFactory<ts.SourceFile>,
) {}
}

Expand Down Expand Up @@ -70,6 +72,7 @@ export class JitCompilation extends AngularCompilation {
typeScriptProgram,
constructorParametersDownlevelTransform(typeScriptProgram.getProgram()),
createJitResourceTransformer(() => typeScriptProgram.getProgram().getTypeChecker()),
createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)),
);

const referencedFiles = typeScriptProgram
Expand Down Expand Up @@ -100,6 +103,7 @@ export class JitCompilation extends AngularCompilation {
typeScriptProgram,
constructorParametersDownlevelTransform,
replaceResourcesTransform,
webWorkerTransform,
} = this.#state;
const buildInfoFilename =
typeScriptProgram.getCompilerOptions().tsBuildInfoFile ?? '.tsbuildinfo';
Expand All @@ -118,7 +122,11 @@ export class JitCompilation extends AngularCompilation {
emittedFiles.push({ filename: sourceFiles[0].fileName, contents });
};
const transformers = {
before: [replaceResourcesTransform, constructorParametersDownlevelTransform],
before: [
replaceResourcesTransform,
constructorParametersDownlevelTransform,
webWorkerTransform,
],
};

// TypeScript will loop until there are no more affected files in the program
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,25 @@ export function createCompilerPlugin(

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.`,
},
],
});

// Returning the original file prevents modification to the containing file
return workerFile;
},
};

// Initialize the Angular compilation for the current build.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import ts from 'typescript';

/**
* Creates a TypeScript Transformer to process Worker and SharedWorker entry points and transform
* the URL instances to reference the built and bundled worker code. This uses a callback process
* similar to the component stylesheets to allow the main esbuild plugin to process files as needed.
* Unsupported worker expressions will be left in their origin form.
* @param getTypeChecker A function that returns a TypeScript TypeChecker instance for the program.
* @returns A TypeScript transformer factory.
*/
export function createWorkerTransformer(
fileProcessor: (file: string, importer: string) => string,
): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => {
const nodeFactory = context.factory;

const visitNode: ts.Visitor = (node: ts.Node) => {
// Check if the node is a valid new expression for a Worker or SharedWorker
// TODO: Add global scope check
if (
!ts.isNewExpression(node) ||
!ts.isIdentifier(node.expression) ||
(node.expression.text !== 'Worker' && node.expression.text !== 'SharedWorker')
) {
// Visit child nodes of non-Worker expressions
return ts.visitEachChild(node, visitNode, context);
}

// Worker should have atleast one argument but not more than two
if (!node.arguments || node.arguments.length < 1 || node.arguments.length > 2) {
return node;
}

// First argument must be a new URL expression
const workerUrlNode = node.arguments[0];
// TODO: Add global scope check
if (
!ts.isNewExpression(workerUrlNode) ||
!ts.isIdentifier(workerUrlNode.expression) ||
workerUrlNode.expression.text !== 'URL'
) {
return node;
}

// URL must have 2 arguments
if (!workerUrlNode.arguments || workerUrlNode.arguments.length !== 2) {
return node;
}

// URL arguments must be a string and then `import.meta.url`
if (
!ts.isStringLiteralLike(workerUrlNode.arguments[0]) ||
!ts.isPropertyAccessExpression(workerUrlNode.arguments[1]) ||
!ts.isMetaProperty(workerUrlNode.arguments[1].expression) ||
workerUrlNode.arguments[1].name.text !== 'url'
) {
return node;
}

const filePath = workerUrlNode.arguments[0].text;
const importer = node.getSourceFile().fileName;

// Process the file
const replacementPath = fileProcessor(filePath, importer);

// Update if the path changed
if (replacementPath !== filePath) {
return nodeFactory.updateNewExpression(
node,
node.expression,
node.typeArguments,
// Update Worker arguments
ts.setTextRange(
nodeFactory.createNodeArray(
[
nodeFactory.updateNewExpression(
workerUrlNode,
workerUrlNode.expression,
workerUrlNode.typeArguments,
// Update URL arguments
ts.setTextRange(
nodeFactory.createNodeArray(
[
nodeFactory.createStringLiteral(replacementPath),
workerUrlNode.arguments[1],
],
workerUrlNode.arguments.hasTrailingComma,
),
workerUrlNode.arguments,
),
),
node.arguments[1],
],
node.arguments.hasTrailingComma,
),
node.arguments,
),
);
} else {
return node;
}
};

return (sourceFile) => {
// Skip transformer if there are no Workers
if (!sourceFile.text.includes('Worker')) {
return sourceFile;
}

return ts.visitEachChild(sourceFile, visitNode, context);
};
};
}

0 comments on commit 8bce80b

Please sign in to comment.