Skip to content

Commit e33a306

Browse files
committed
feat(@schematics/angular): migrate web workers to support Webpack 5
Webpack 5 now includes web worker support. However, the structure of the URL within the `Worker` constructor must be in a specific format. A migration has been added for Angular v12 that will convert web workers from the old structure to the new structure. Before: `new Worker('./app.worker', ...)` After: `new Worker(new URL('./app.worker', import.meta.url), ...)`
1 parent d68cb92 commit e33a306

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed

packages/schematics/angular/migrations/migration-collection.json

+5
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@
110110
"factory": "./update-9/update-i18n#updateI18nConfig",
111111
"description": "Remove deprecated ViewEngine-based i18n build and extract options. Options present in the configuration will be converted to use non-deprecated options."
112112
},
113+
"update-web-workers-webpack-5": {
114+
"version": "12.0.0-next.7",
115+
"factory": "./update-12/update-web-workers",
116+
"description": "Updates Web Worker consumer usage to use the new syntax supported directly by Webpack 5."
117+
},
113118
"production-by-default": {
114119
"version": "9999.0.0",
115120
"factory": "./update-12/production-default-config",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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+
import { DirEntry, Rule, UpdateRecorder } from '@angular-devkit/schematics';
9+
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
10+
11+
function* visit(directory: DirEntry): IterableIterator<ts.SourceFile> {
12+
for (const path of directory.subfiles) {
13+
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
14+
const entry = directory.file(path);
15+
if (entry) {
16+
const content = entry.content;
17+
if (content.includes('Worker')) {
18+
const source = ts.createSourceFile(
19+
entry.path,
20+
// Remove UTF-8 BOM if present
21+
// TypeScript expects the BOM to be stripped prior to parsing
22+
content.toString().replace(/^\uFEFF/, ''),
23+
ts.ScriptTarget.Latest,
24+
true,
25+
);
26+
27+
yield source;
28+
}
29+
}
30+
}
31+
}
32+
33+
for (const path of directory.subdirs) {
34+
if (path === 'node_modules' || path.startsWith('.')) {
35+
continue;
36+
}
37+
38+
yield* visit(directory.dir(path));
39+
}
40+
}
41+
42+
function hasPropertyWithValue(node: ts.Expression, name: string, value: unknown): boolean {
43+
if (!ts.isObjectLiteralExpression(node)) {
44+
return false;
45+
}
46+
47+
for (const property of node.properties) {
48+
if (!ts.isPropertyAssignment(property)) {
49+
continue;
50+
}
51+
if (!ts.isIdentifier(property.name) || property.name.text !== 'type') {
52+
continue;
53+
}
54+
if (ts.isStringLiteralLike(property.initializer)) {
55+
return property.initializer.text === 'module';
56+
}
57+
}
58+
59+
return false;
60+
}
61+
62+
export default function (): Rule {
63+
return (tree) => {
64+
for (const sourceFile of visit(tree.root)) {
65+
let recorder: UpdateRecorder | undefined;
66+
67+
ts.forEachChild(sourceFile, function analyze(node) {
68+
// Only modify code in the form of `new Worker('./app.worker', { type: 'module' })`.
69+
// `worker-plugin` required the second argument to be an object literal with type=module
70+
if (
71+
ts.isNewExpression(node) &&
72+
ts.isIdentifier(node.expression) &&
73+
node.expression.text === 'Worker' &&
74+
node.arguments?.length === 2 &&
75+
ts.isStringLiteralLike(node.arguments[0]) &&
76+
hasPropertyWithValue(node.arguments[1], 'type', 'module')
77+
) {
78+
const valueNode = node.arguments[0] as ts.StringLiteralLike;
79+
80+
// Webpack expects a URL constructor: https://webpack.js.org/guides/web-workers/
81+
const fix = `new URL('${valueNode.text}', import.meta.url)`;
82+
83+
if (!recorder) {
84+
recorder = tree.beginUpdate(sourceFile.fileName);
85+
}
86+
87+
const index = valueNode.getStart();
88+
const length = valueNode.getWidth();
89+
recorder.remove(index, length).insertLeft(index, fix);
90+
}
91+
92+
ts.forEachChild(node, analyze);
93+
});
94+
95+
if (recorder) {
96+
tree.commitUpdate(recorder);
97+
}
98+
}
99+
};
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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+
import { EmptyTree } from '@angular-devkit/schematics';
9+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
10+
11+
describe('Migration to update Web Workers for Webpack 5', () => {
12+
const schematicRunner = new SchematicTestRunner(
13+
'migrations',
14+
require.resolve('../migration-collection.json'),
15+
);
16+
17+
let tree: UnitTestTree;
18+
19+
const workerConsumerPath = 'src/consumer.ts';
20+
const workerConsumerContent = `
21+
import { enableProdMode } from '@angular/core';
22+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
23+
import { AppModule } from './app/app.module';
24+
import { environment } from './environments/environment';
25+
if (environment.production) { enableProdMode(); }
26+
platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.error(err));
27+
28+
const worker = new Worker('./app/app.worker', { type: 'module' });
29+
worker.onmessage = ({ data }) => {
30+
console.log('page got message:', data);
31+
};
32+
worker.postMessage('hello');
33+
`;
34+
35+
beforeEach(async () => {
36+
tree = new UnitTestTree(new EmptyTree());
37+
tree.create('/package.json', JSON.stringify({}));
38+
});
39+
40+
it('should replace the string path argument with a URL constructor', async () => {
41+
tree.create(workerConsumerPath, workerConsumerContent);
42+
43+
await schematicRunner.runSchematicAsync('update-web-workers-webpack-5', {}, tree).toPromise();
44+
await schematicRunner.engine.executePostTasks().toPromise();
45+
46+
const consumer = tree.readContent(workerConsumerPath);
47+
48+
expect(consumer).not.toContain(`new Worker('./app/app.worker'`);
49+
expect(consumer).toContain(
50+
`new Worker(new URL('./app/app.worker', import.meta.url), { type: 'module' });`,
51+
);
52+
});
53+
54+
it('should not replace the first argument if arguments types are invalid', async () => {
55+
tree.create(workerConsumerPath, workerConsumerContent.replace(`'./app/app.worker'`, '42'));
56+
57+
await schematicRunner.runSchematicAsync('update-web-workers-webpack-5', {}, tree).toPromise();
58+
await schematicRunner.engine.executePostTasks().toPromise();
59+
60+
const consumer = tree.readContent(workerConsumerPath);
61+
62+
expect(consumer).toContain(`new Worker(42`);
63+
expect(consumer).not.toContain(
64+
`new Worker(new URL('42', import.meta.url), { type: 'module' });`,
65+
);
66+
});
67+
68+
it('should not replace the first argument if type value is not "module"', async () => {
69+
tree.create(workerConsumerPath, workerConsumerContent.replace(`type: 'module'`, `type: 'xyz'`));
70+
71+
await schematicRunner.runSchematicAsync('update-web-workers-webpack-5', {}, tree).toPromise();
72+
await schematicRunner.engine.executePostTasks().toPromise();
73+
74+
const consumer = tree.readContent(workerConsumerPath);
75+
76+
expect(consumer).toContain(`new Worker('./app/app.worker'`);
77+
expect(consumer).not.toContain(
78+
`new Worker(new URL('42', import.meta.url), { type: 'xyz' });`,
79+
);
80+
});
81+
82+
it('should replace the module path string when file has BOM', async () => {
83+
tree.create(workerConsumerPath, '\uFEFF' + workerConsumerContent);
84+
85+
await schematicRunner.runSchematicAsync('update-web-workers-webpack-5', {}, tree).toPromise();
86+
await schematicRunner.engine.executePostTasks().toPromise();
87+
88+
const consumer = tree.readContent(workerConsumerPath);
89+
90+
expect(consumer).not.toContain(`new Worker('./app/app.worker'`);
91+
expect(consumer).toContain(
92+
`new Worker(new URL('./app/app.worker', import.meta.url), { type: 'module' });`,
93+
);
94+
});
95+
});

0 commit comments

Comments
 (0)