-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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), ...)`
- Loading branch information
Showing
3 changed files
with
200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
packages/schematics/angular/migrations/update-12/update-web-workers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 { DirEntry, Rule, UpdateRecorder } from '@angular-devkit/schematics'; | ||
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; | ||
|
||
function* visit(directory: DirEntry): IterableIterator<ts.SourceFile> { | ||
for (const path of directory.subfiles) { | ||
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { | ||
const entry = directory.file(path); | ||
if (entry) { | ||
const content = entry.content; | ||
if (content.includes('Worker')) { | ||
const source = ts.createSourceFile( | ||
entry.path, | ||
// Remove UTF-8 BOM if present | ||
// TypeScript expects the BOM to be stripped prior to parsing | ||
content.toString().replace(/^\uFEFF/, ''), | ||
ts.ScriptTarget.Latest, | ||
true, | ||
); | ||
|
||
yield source; | ||
} | ||
} | ||
} | ||
} | ||
|
||
for (const path of directory.subdirs) { | ||
if (path === 'node_modules' || path.startsWith('.')) { | ||
continue; | ||
} | ||
|
||
yield* visit(directory.dir(path)); | ||
} | ||
} | ||
|
||
function hasPropertyWithValue(node: ts.Expression, name: string, value: unknown): boolean { | ||
if (!ts.isObjectLiteralExpression(node)) { | ||
return false; | ||
} | ||
|
||
for (const property of node.properties) { | ||
if (!ts.isPropertyAssignment(property)) { | ||
continue; | ||
} | ||
if (!ts.isIdentifier(property.name) || property.name.text !== 'type') { | ||
continue; | ||
} | ||
if (ts.isStringLiteralLike(property.initializer)) { | ||
return property.initializer.text === 'module'; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
export default function (): Rule { | ||
return (tree) => { | ||
for (const sourceFile of visit(tree.root)) { | ||
let recorder: UpdateRecorder | undefined; | ||
|
||
ts.forEachChild(sourceFile, function analyze(node) { | ||
// Only modify code in the form of `new Worker('./app.worker', { type: 'module' })`. | ||
// `worker-plugin` required the second argument to be an object literal with type=module | ||
if ( | ||
ts.isNewExpression(node) && | ||
ts.isIdentifier(node.expression) && | ||
node.expression.text === 'Worker' && | ||
node.arguments?.length === 2 && | ||
ts.isStringLiteralLike(node.arguments[0]) && | ||
hasPropertyWithValue(node.arguments[1], 'type', 'module') | ||
) { | ||
const valueNode = node.arguments[0] as ts.StringLiteralLike; | ||
|
||
// Webpack expects a URL constructor: https://webpack.js.org/guides/web-workers/ | ||
const fix = `new URL('${valueNode.text}', import.meta.url)`; | ||
|
||
if (!recorder) { | ||
recorder = tree.beginUpdate(sourceFile.fileName); | ||
} | ||
|
||
const index = valueNode.getStart(); | ||
const length = valueNode.getWidth(); | ||
recorder.remove(index, length).insertLeft(index, fix); | ||
} | ||
|
||
ts.forEachChild(node, analyze); | ||
}); | ||
|
||
if (recorder) { | ||
tree.commitUpdate(recorder); | ||
} | ||
} | ||
}; | ||
} |
95 changes: 95 additions & 0 deletions
95
packages/schematics/angular/migrations/update-12/update-web-workers_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 { EmptyTree } from '@angular-devkit/schematics'; | ||
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; | ||
|
||
describe('Migration to update Web Workers for Webpack 5', () => { | ||
const schematicRunner = new SchematicTestRunner( | ||
'migrations', | ||
require.resolve('../migration-collection.json'), | ||
); | ||
|
||
let tree: UnitTestTree; | ||
|
||
const workerConsumerPath = 'src/consumer.ts'; | ||
const workerConsumerContent = ` | ||
import { enableProdMode } from '@angular/core'; | ||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | ||
import { AppModule } from './app/app.module'; | ||
import { environment } from './environments/environment'; | ||
if (environment.production) { enableProdMode(); } | ||
platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.error(err)); | ||
const worker = new Worker('./app/app.worker', { type: 'module' }); | ||
worker.onmessage = ({ data }) => { | ||
console.log('page got message:', data); | ||
}; | ||
worker.postMessage('hello'); | ||
`; | ||
|
||
beforeEach(async () => { | ||
tree = new UnitTestTree(new EmptyTree()); | ||
tree.create('/package.json', JSON.stringify({})); | ||
}); | ||
|
||
it('should replace the string path argument with a URL constructor', async () => { | ||
tree.create(workerConsumerPath, workerConsumerContent); | ||
|
||
await schematicRunner.runSchematicAsync('update-web-workers-webpack-5', {}, tree).toPromise(); | ||
await schematicRunner.engine.executePostTasks().toPromise(); | ||
|
||
const consumer = tree.readContent(workerConsumerPath); | ||
|
||
expect(consumer).not.toContain(`new Worker('./app/app.worker'`); | ||
expect(consumer).toContain( | ||
`new Worker(new URL('./app/app.worker', import.meta.url), { type: 'module' });`, | ||
); | ||
}); | ||
|
||
it('should not replace the first argument if arguments types are invalid', async () => { | ||
tree.create(workerConsumerPath, workerConsumerContent.replace(`'./app/app.worker'`, '42')); | ||
|
||
await schematicRunner.runSchematicAsync('update-web-workers-webpack-5', {}, tree).toPromise(); | ||
await schematicRunner.engine.executePostTasks().toPromise(); | ||
|
||
const consumer = tree.readContent(workerConsumerPath); | ||
|
||
expect(consumer).toContain(`new Worker(42`); | ||
expect(consumer).not.toContain( | ||
`new Worker(new URL('42', import.meta.url), { type: 'module' });`, | ||
); | ||
}); | ||
|
||
it('should not replace the first argument if type value is not "module"', async () => { | ||
tree.create(workerConsumerPath, workerConsumerContent.replace(`type: 'module'`, `type: 'xyz'`)); | ||
|
||
await schematicRunner.runSchematicAsync('update-web-workers-webpack-5', {}, tree).toPromise(); | ||
await schematicRunner.engine.executePostTasks().toPromise(); | ||
|
||
const consumer = tree.readContent(workerConsumerPath); | ||
|
||
expect(consumer).toContain(`new Worker('./app/app.worker'`); | ||
expect(consumer).not.toContain( | ||
`new Worker(new URL('42', import.meta.url), { type: 'xyz' });`, | ||
); | ||
}); | ||
|
||
it('should replace the module path string when file has BOM', async () => { | ||
tree.create(workerConsumerPath, '\uFEFF' + workerConsumerContent); | ||
|
||
await schematicRunner.runSchematicAsync('update-web-workers-webpack-5', {}, tree).toPromise(); | ||
await schematicRunner.engine.executePostTasks().toPromise(); | ||
|
||
const consumer = tree.readContent(workerConsumerPath); | ||
|
||
expect(consumer).not.toContain(`new Worker('./app/app.worker'`); | ||
expect(consumer).toContain( | ||
`new Worker(new URL('./app/app.worker', import.meta.url), { type: 'module' });`, | ||
); | ||
}); | ||
}); |