Skip to content

Commit 651a873

Browse files
committed
feat(@angular-devkit/build-angular): support TS web workers
1 parent 7c1b045 commit 651a873

File tree

10 files changed

+262
-60
lines changed

10 files changed

+262
-60
lines changed

Diff for: packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface BuildOptions {
6060
forkTypeChecker: boolean;
6161
profile?: boolean;
6262
es5BrowserSupport?: boolean;
63+
workerTsConfig?: string;
6364

6465
main: string;
6566
index: string;

Diff for: packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts

-6
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ const ProgressPlugin = require('webpack/lib/ProgressPlugin');
2222
const CircularDependencyPlugin = require('circular-dependency-plugin');
2323
const TerserPlugin = require('terser-webpack-plugin');
2424
const StatsPlugin = require('stats-webpack-plugin');
25-
const WorkerPlugin = require('worker-plugin');
2625

2726

2827
// tslint:disable-next-line:no-any
@@ -128,11 +127,6 @@ export function getCommonConfig(wco: WebpackConfigOptions) {
128127
});
129128
}
130129

131-
if (buildOptions.autoBundleWorkerModules) {
132-
const workerPluginInstance = new WorkerPlugin({ globalObject: false });
133-
extraPlugins.push(workerPluginInstance);
134-
}
135-
136130
// process asset entries
137131
if (buildOptions.assets) {
138132
const copyWebpackPluginPatterns = buildOptions.assets.map((asset: AssetPatternObject) => {

Diff for: packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './test';
1313
export * from './typescript';
1414
export * from './utils';
1515
export * from './stats';
16+
export * from './worker';

Diff for: packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts

+17
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,20 @@ export function getNonAotTestConfig(wco: WebpackConfigOptions, host: virtualFs.H
123123
plugins: [_createAotPlugin(wco, { tsConfigPath, skipCodeGeneration: true }, host, false)]
124124
};
125125
}
126+
127+
export function getTypescriptWorkerPlugin(wco: WebpackConfigOptions, workerTsConfigPath: string) {
128+
const { buildOptions } = wco;
129+
130+
const pluginOptions: AngularCompilerPluginOptions = {
131+
skipCodeGeneration: true,
132+
tsConfigPath: workerTsConfigPath,
133+
mainPath: undefined,
134+
platform: PLATFORM.Browser,
135+
sourceMap: buildOptions.sourceMap.scripts,
136+
forkTypeChecker: buildOptions.forkTypeChecker,
137+
contextElementDependencyConstructor: require('webpack/lib/dependencies/ContextElementDependency'),
138+
logger: wco.logger,
139+
};
140+
141+
return new AngularCompilerPlugin(pluginOptions);
142+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 { resolve } from 'path';
9+
import { Configuration } from 'webpack';
10+
import { WebpackConfigOptions } from '../build-options';
11+
import { getTypescriptWorkerPlugin } from './typescript';
12+
13+
const WorkerPlugin = require('worker-plugin');
14+
15+
16+
export function getWorkerConfig(wco: WebpackConfigOptions): Configuration {
17+
const { buildOptions } = wco;
18+
const workerTsConfigPath = buildOptions.workerTsConfig
19+
? resolve(wco.root, buildOptions.workerTsConfig)
20+
: undefined;
21+
22+
return {
23+
plugins: [new WorkerPlugin({
24+
globalObject: false,
25+
plugins: workerTsConfigPath ? [getTypescriptWorkerPlugin(wco, workerTsConfigPath)] : [],
26+
})],
27+
};
28+
}

Diff for: packages/angular_devkit/build_angular/src/browser/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
getNonAotConfig,
2626
getStatsConfig,
2727
getStylesConfig,
28+
getWorkerConfig,
2829
} from '../angular-cli-files/models/webpack-configs';
2930
import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig';
3031
import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module';
@@ -152,6 +153,10 @@ export class BrowserBuilder implements Builder<BrowserBuilderSchema> {
152153
webpackConfigs.push(typescriptConfigPartial);
153154
}
154155

156+
if (wco.buildOptions.autoBundleWorkerModules) {
157+
webpackConfigs.push(getWorkerConfig(wco));
158+
}
159+
155160
const webpackConfig = webpackMerge(webpackConfigs);
156161

157162
if (options.profile) {

Diff for: packages/angular_devkit/build_angular/src/browser/schema.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,11 @@ export interface BrowserBuilderSchema {
199199
*/
200200
autoBundleWorkerModules: boolean;
201201

202+
/**
203+
* TypeScript configuration for worker modules.
204+
*/
205+
workerTsConfig?: string;
206+
202207
/**
203208
* Path to ngsw-config.json.
204209
*/

Diff for: packages/angular_devkit/build_angular/src/browser/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@
260260
"description": "Automatically bundle new Worker('..', { type:'module' })",
261261
"default": true
262262
},
263+
"workerTsConfig": {
264+
"type": "string",
265+
"description": "TypeScript configuration for worker modules."
266+
},
263267
"ngswConfigPath": {
264268
"type": "string",
265269
"description": "Path to ngsw-config.json."

Diff for: packages/angular_devkit/build_angular/test/browser/bundle-worker_spec_large.ts

+136-54
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { runTargetSpec } from '@angular-devkit/architect/testing';
9+
import { TestLogger, runTargetSpec } from '@angular-devkit/architect/testing';
1010
import { join, virtualFs } from '@angular-devkit/core';
1111
import { tap } from 'rxjs/operators';
1212
import { browserTargetSpec, host, outputPath } from '../utils';
1313

1414

1515
describe('Browser Builder bundle worker', () => {
1616
beforeEach(done => host.initialize().toPromise().then(done, done.fail));
17-
// afterEach(done => host.restore().toPromise().then(done, done.fail));
17+
afterEach(done => host.restore().toPromise().then(done, done.fail));
1818

19-
const workerFiles = {
20-
'src/dep.js': `export const foo = 'bar';`,
21-
'src/worker.js': `
19+
const workerFiles: {[k: string]: string } = {
20+
'src/worker/dep.js': `export const foo = 'bar';`,
21+
'src/worker/worker.js': `
2222
import { foo } from './dep';
2323
2424
console.log('hello from worker');
@@ -31,61 +31,143 @@ describe('Browser Builder bundle worker', () => {
3131
});
3232
`,
3333
'src/main.ts': `
34-
const worker = new Worker('./worker', { type: 'module' });
34+
import { enableProdMode } from '@angular/core';
35+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
36+
37+
import { AppModule } from './app/app.module';
38+
import { environment } from './environments/environment';
39+
40+
if (environment.production) {
41+
enableProdMode();
42+
}
43+
44+
platformBrowserDynamic().bootstrapModule(AppModule)
45+
.catch(err => console.error(err));
46+
47+
const worker = new Worker('./worker/worker.js', { type: 'module' });
3548
worker.onmessage = ({ data }) => {
3649
console.log('page got message:', data);
3750
};
3851
worker.postMessage('hello');
3952
`,
4053
};
4154

42-
describe('js workers', () => {
43-
it('bundles worker', (done) => {
44-
host.writeMultipleFiles(workerFiles);
45-
const overrides = { autoBundleWorkerModules: true };
46-
runTargetSpec(host, browserTargetSpec, overrides).pipe(
47-
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
48-
tap(() => {
49-
const workerContent = virtualFs.fileBufferToString(
50-
host.scopedSync().read(join(outputPath, '0.worker.js')),
51-
);
52-
// worker bundle contains worker code.
53-
expect(workerContent).toContain('hello from worker');
54-
expect(workerContent).toContain('bar');
55-
56-
const mainContent = virtualFs.fileBufferToString(
57-
host.scopedSync().read(join(outputPath, 'main.js')),
58-
);
59-
// main bundle references worker.
60-
expect(mainContent).toContain('0.worker.js');
61-
}),
62-
).toPromise().then(done, done.fail);
63-
});
64-
65-
it('minimizes and hashes worker', (done) => {
66-
host.writeMultipleFiles(workerFiles);
67-
const overrides = { autoBundleWorkerModules: true, outputHashing: 'all', optimization: true };
68-
runTargetSpec(host, browserTargetSpec, overrides).pipe(
69-
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
70-
tap(() => {
71-
const workerBundle = host.fileMatchExists(outputPath,
72-
/0\.[0-9a-f]{20}\.worker\.js/) as string;
73-
expect(workerBundle).toBeTruthy('workerBundle should exist');
74-
const workerContent = virtualFs.fileBufferToString(
75-
host.scopedSync().read(join(outputPath, workerBundle)),
76-
);
77-
expect(workerContent).toContain('hello from worker');
78-
expect(workerContent).toContain('bar');
79-
expect(workerContent).toContain('"hello"===e&&postMessage("bar")');
80-
81-
const mainBundle = host.fileMatchExists(outputPath, /main\.[0-9a-f]{20}\.js/) as string;
82-
expect(mainBundle).toBeTruthy('mainBundle should exist');
83-
const mainContent = virtualFs.fileBufferToString(
84-
host.scopedSync().read(join(outputPath, mainBundle)),
85-
);
86-
expect(mainContent).toContain(workerBundle);
87-
}),
88-
).toPromise().then(done, done.fail);
89-
});
55+
it('bundles worker', (done) => {
56+
host.writeMultipleFiles(workerFiles);
57+
const overrides = { autoBundleWorkerModules: true };
58+
runTargetSpec(host, browserTargetSpec, overrides).pipe(
59+
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
60+
tap(() => {
61+
const workerContent = virtualFs.fileBufferToString(
62+
host.scopedSync().read(join(outputPath, '0.worker.js')),
63+
);
64+
// worker bundle contains worker code.
65+
expect(workerContent).toContain('hello from worker');
66+
expect(workerContent).toContain('bar');
67+
68+
const mainContent = virtualFs.fileBufferToString(
69+
host.scopedSync().read(join(outputPath, 'main.js')),
70+
);
71+
// main bundle references worker.
72+
expect(mainContent).toContain('0.worker.js');
73+
}),
74+
).toPromise().then(done, done.fail);
75+
});
76+
77+
it('minimizes and hashes worker', (done) => {
78+
host.writeMultipleFiles(workerFiles);
79+
const overrides = { autoBundleWorkerModules: true, outputHashing: 'all', optimization: true };
80+
runTargetSpec(host, browserTargetSpec, overrides).pipe(
81+
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
82+
tap(() => {
83+
const workerBundle = host.fileMatchExists(outputPath,
84+
/0\.[0-9a-f]{20}\.worker\.js/) as string;
85+
expect(workerBundle).toBeTruthy('workerBundle should exist');
86+
const workerContent = virtualFs.fileBufferToString(
87+
host.scopedSync().read(join(outputPath, workerBundle)),
88+
);
89+
expect(workerContent).toContain('hello from worker');
90+
expect(workerContent).toContain('bar');
91+
expect(workerContent).toContain('"hello"===e&&postMessage("bar")');
92+
93+
const mainBundle = host.fileMatchExists(outputPath, /main\.[0-9a-f]{20}\.js/) as string;
94+
expect(mainBundle).toBeTruthy('mainBundle should exist');
95+
const mainContent = virtualFs.fileBufferToString(
96+
host.scopedSync().read(join(outputPath, mainBundle)),
97+
);
98+
expect(mainContent).toContain(workerBundle);
99+
}),
100+
).toPromise().then(done, done.fail);
101+
});
102+
103+
it('bundles TS worker', (done) => {
104+
// Use the same worker file content but in a .ts file name.
105+
const tsWorkerFiles = Object.keys(workerFiles)
106+
.reduce((acc, k) => {
107+
// Replace the .js files with .ts, and also references within the files.
108+
acc[k.replace(/\.js$/, '.ts')] = workerFiles[k].replace(/\.js'/g, `.ts'`);
109+
110+
return acc;
111+
}, {} as { [k: string]: string });
112+
host.writeMultipleFiles(tsWorkerFiles);
113+
114+
host.writeMultipleFiles({
115+
// Make a new tsconfig for the worker folder that includes the webworker lib.
116+
// The final place for this tsconfig must take into consideration editor tooling, unit
117+
// tests, and integration with other build targets.
118+
'./src/worker/tsconfig.json': `
119+
{
120+
"extends": "../../tsconfig.json",
121+
"compilerOptions": {
122+
"outDir": "../../out-tsc/worker",
123+
"lib": [
124+
"es2018",
125+
"webworker"
126+
],
127+
"types": []
128+
}
129+
}`,
130+
// Alter the app tsconfig to not include worker files.
131+
'./src/tsconfig.app.json': `
132+
{
133+
"extends": "../tsconfig.json",
134+
"compilerOptions": {
135+
"outDir": "../out-tsc/worker",
136+
"types": []
137+
},
138+
"exclude": [
139+
"test.ts",
140+
"**/*.spec.ts",
141+
"worker/**/*.ts"
142+
]
143+
}`,
144+
});
145+
146+
const overrides = {
147+
autoBundleWorkerModules: true,
148+
workerTsConfig: 'src/worker/tsconfig.json',
149+
};
150+
151+
const logger = new TestLogger('worker-warnings');
152+
153+
runTargetSpec(host, browserTargetSpec, overrides, 45000, logger).pipe(
154+
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
155+
tap(() => {
156+
const workerContent = virtualFs.fileBufferToString(
157+
host.scopedSync().read(join(outputPath, '0.worker.js')),
158+
);
159+
// worker bundle contains worker code.
160+
expect(workerContent).toContain('hello from worker');
161+
expect(workerContent).toContain('bar');
162+
163+
const mainContent = virtualFs.fileBufferToString(
164+
host.scopedSync().read(join(outputPath, 'main.js')),
165+
);
166+
// main bundle references worker.
167+
expect(mainContent).toContain('0.worker.js');
168+
}),
169+
// Doesn't show any warnings.
170+
tap(() => expect(logger.includes('WARNING')).toBe(false, 'Should show no warnings.')),
171+
).toPromise().then(done, done.fail);
90172
});
91173
});

0 commit comments

Comments
 (0)