-
Notifications
You must be signed in to change notification settings - Fork 12k
Web Worker support #13700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Web Worker support #13700
Changes from all commits
d9249c9
cf3171f
224cbe1
ced9890
f294851
4c92bad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/** | ||
* @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 { resolve } from 'path'; | ||
import { Configuration } from 'webpack'; | ||
import { WebpackConfigOptions } from '../build-options'; | ||
import { getTypescriptWorkerPlugin } from './typescript'; | ||
|
||
const WorkerPlugin = require('worker-plugin'); | ||
|
||
|
||
export function getWorkerConfig(wco: WebpackConfigOptions): Configuration { | ||
const { buildOptions } = wco; | ||
if (!buildOptions.webWorkerTsConfig) { | ||
throw new Error('The `webWorkerTsConfig` must be a string.'); | ||
} | ||
|
||
const workerTsConfigPath = resolve(wco.root, buildOptions.webWorkerTsConfig); | ||
|
||
return { | ||
plugins: [new WorkerPlugin({ | ||
filipesilva marked this conversation as resolved.
Show resolved
Hide resolved
|
||
globalObject: false, | ||
plugins: [getTypescriptWorkerPlugin(wco, workerTsConfigPath)], | ||
})], | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
/** | ||
* @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 { Architect } from '@angular-devkit/architect/src/index2'; | ||
import { TestLogger } from '@angular-devkit/architect/testing'; | ||
import { join, virtualFs } from '@angular-devkit/core'; | ||
import { debounceTime, takeWhile, tap } from 'rxjs/operators'; | ||
import { browserBuild, createArchitect, host, outputPath } from '../utils'; | ||
|
||
|
||
describe('Browser Builder Web Worker support', () => { | ||
const target = { project: 'app', target: 'build' }; | ||
let architect: Architect; | ||
|
||
beforeEach(async () => { | ||
await host.initialize().toPromise(); | ||
architect = (await createArchitect(host.root())).architect; | ||
}); | ||
afterEach(async () => host.restore().toPromise()); | ||
|
||
const workerFiles: { [k: string]: string } = { | ||
'src/app/dep.ts': `export const foo = 'bar';`, | ||
'src/app/app.worker.ts': ` | ||
import { foo } from './dep'; | ||
console.log('hello from worker'); | ||
addEventListener('message', ({ data }) => { | ||
console.log('worker got message:', data); | ||
if (data === 'hello') { | ||
postMessage(foo); | ||
} | ||
}); | ||
`, | ||
'src/main.ts': ` | ||
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'); | ||
`, | ||
// Make a new tsconfig for the *.worker.ts files. | ||
// The final place for this tsconfig must take into consideration editor tooling, unit | ||
// tests, and integration with other build targets. | ||
'./src/tsconfig.worker.json': ` | ||
{ | ||
"extends": "../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "../out-tsc/worker", | ||
"lib": [ | ||
"es2018", | ||
"webworker" | ||
], | ||
"types": [] | ||
}, | ||
"include": [ | ||
"**/*.worker.ts", | ||
] | ||
}`, | ||
// Alter the app tsconfig to not include *.worker.ts files. | ||
'./src/tsconfig.app.json': ` | ||
{ | ||
"extends": "../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "../out-tsc/worker", | ||
"types": [] | ||
}, | ||
"exclude": [ | ||
"test.ts", | ||
"**/*.spec.ts", | ||
"**/*.worker.ts", | ||
"app/dep.ts", | ||
] | ||
}`, | ||
}; | ||
|
||
it('bundles TS worker', async () => { | ||
host.writeMultipleFiles(workerFiles); | ||
const logger = new TestLogger('worker-warnings'); | ||
const overrides = { webWorkerTsConfig: 'src/tsconfig.worker.json' }; | ||
await browserBuild(architect, host, target, overrides, { logger }); | ||
|
||
// Worker bundle contains worker code. | ||
const workerContent = virtualFs.fileBufferToString( | ||
host.scopedSync().read(join(outputPath, '0.worker.js'))); | ||
expect(workerContent).toContain('hello from worker'); | ||
expect(workerContent).toContain('bar'); | ||
|
||
// Main bundle references worker. | ||
const mainContent = virtualFs.fileBufferToString( | ||
host.scopedSync().read(join(outputPath, 'main.js'))); | ||
expect(mainContent).toContain('0.worker.js'); | ||
expect(logger.includes('WARNING')).toBe(false, 'Should show no warnings.'); | ||
}); | ||
|
||
it('minimizes and hashes worker', async () => { | ||
host.writeMultipleFiles(workerFiles); | ||
const overrides = { | ||
webWorkerTsConfig: 'src/tsconfig.worker.json', | ||
outputHashing: 'all', | ||
optimization: true, | ||
}; | ||
await browserBuild(architect, host, target, overrides); | ||
|
||
// Worker bundle should have hash and minified code. | ||
const workerBundle = host.fileMatchExists(outputPath, /0\.[0-9a-f]{20}\.worker\.js/) as string; | ||
expect(workerBundle).toBeTruthy('workerBundle should exist'); | ||
const workerContent = virtualFs.fileBufferToString( | ||
host.scopedSync().read(join(outputPath, workerBundle))); | ||
expect(workerContent).toContain('hello from worker'); | ||
expect(workerContent).toContain('bar'); | ||
expect(workerContent).toContain('"hello"===t&&postMessage'); | ||
|
||
// Main bundle should reference hashed worker bundle. | ||
const mainBundle = host.fileMatchExists(outputPath, /main\.[0-9a-f]{20}\.js/) as string; | ||
expect(mainBundle).toBeTruthy('mainBundle should exist'); | ||
const mainContent = virtualFs.fileBufferToString( | ||
host.scopedSync().read(join(outputPath, mainBundle))); | ||
expect(mainContent).toContain(workerBundle); | ||
}); | ||
|
||
it('rebuilds TS worker', async () => { | ||
host.writeMultipleFiles(workerFiles); | ||
const overrides = { | ||
webWorkerTsConfig: 'src/tsconfig.worker.json', | ||
watch: true, | ||
}; | ||
|
||
let buildCount = 0; | ||
let phase = 1; | ||
const workerPath = join(outputPath, '0.worker.js'); | ||
let workerContent = ''; | ||
|
||
const run = await architect.scheduleTarget(target, overrides); | ||
await run.output.pipe( | ||
// Wait for files to be written to disk. | ||
debounceTime(1000), | ||
tap((buildEvent) => expect(buildEvent.success).toBe(true, 'build should succeed')), | ||
tap(() => { | ||
buildCount++; | ||
switch (phase) { | ||
case 1: | ||
// Original worker content should be there. | ||
workerContent = virtualFs.fileBufferToString(host.scopedSync().read(workerPath)); | ||
expect(workerContent).toContain('bar'); | ||
// Change content of worker dependency. | ||
host.writeMultipleFiles({ 'src/app/dep.ts': `export const foo = 'baz';` }); | ||
phase = 2; | ||
break; | ||
|
||
case 2: | ||
workerContent = virtualFs.fileBufferToString(host.scopedSync().read(workerPath)); | ||
// TODO(filipesilva): Should this change? Check with Jason Miller. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @developit figured out the rebuild issue. It's because the worker changes name on rebuilds. Is this intended? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm also seeing a weird behaviour where sometimes the child compilation picks up on changed files, and sometimes it doesn't. In this test I have seen all these outputs just by running the rebuild test repeatedly with no code changes:
Do you have any idea of what's happening here? Is there some cache inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the worker changing names may be somewhat unavoidable. There's a There's no caching enabled in worker-plugin, so it's likely something deeper in Webpack. I'm going to add some rebuild tests to the plugin to see if I can trigger this in isolation. My hunch is that the parser hook's instance counter isn't being reset because the parser is reused across webpack compilations. It would certainly explain the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Been trying to debug further and have some findings. In our test suite we run webpack several times in the same thread. I think the test order affects the result of rebuilds with To test this I added a lot of debug logs and tried running the basic test and the rebuild test in different orders. Then I collected the logs just from the rebuild test. One of the things I logged was the entry for the child compilation: https://github.com/GoogleChromeLabs/worker-plugin/blob/7ea61fb83a02170d0235e4590c4deb617874e70a/src/loader.js#L77. The others were build count, changed file being loaded, and which bundle contains the changed content. If I run the rebuild test first and the basic test second I see:
If I run the rebuild test first and the basic test second I see:
So the end result is that the order of the tests seems to affect which bundle ends up containing the changed code. But more interestingly, when the rebuild test is second, before the first build finishes there are a total of two child compilations that run. That part I find rather odd. I don't understand why that would happen. Even weirder is that the next child compilation is again the |
||
// The worker changes name with each rebuild. But sometimes it also changes from 0 to | ||
// 1 and then back to 0. It's hard to know where the updated content is, but it should | ||
// be in one of these two. | ||
const anotherWorkerPath = join(outputPath, '1.worker.js'); | ||
const anotherWorkerContent = virtualFs.fileBufferToString( | ||
host.scopedSync().read(anotherWorkerPath)); | ||
// Worker content should have changed. | ||
expect( | ||
workerContent.includes('baz') || anotherWorkerContent.includes('baz'), | ||
).toBeTruthy('Worker bundle did not contain updated content.'); | ||
phase = 3; | ||
break; | ||
} | ||
}), | ||
takeWhile(() => phase < 3), | ||
).toPromise(); | ||
await run.stop(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json", | ||
"compilerOptions": { | ||
"lib": [ | ||
"es2018", | ||
"dom", | ||
"webworker" | ||
], | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.