Skip to content

Commit 2cd6272

Browse files
committed
feat(@schematics/angular): add web worker schematics
1 parent 6398428 commit 2cd6272

File tree

10 files changed

+420
-87
lines changed

10 files changed

+420
-87
lines changed

packages/schematics/angular/collection.json

+6
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@
100100
"factory": "./library",
101101
"schema": "./library/schema.json",
102102
"description": "Generate a library project for Angular."
103+
},
104+
"webWorker": {
105+
"aliases": ["web-worker", "worker"],
106+
"factory": "./web-worker",
107+
"schema": "./web-worker/schema.json",
108+
"description": "Create a Web Worker ."
103109
}
104110
}
105111
}

packages/schematics/angular/utility/workspace-models.ts

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export interface BrowserBuilderOptions extends BrowserBuilderBaseOptions {
6161
maximumError?: string;
6262
}[];
6363
es5BrowserSupport?: boolean;
64+
webWorkerTsConfig?: string;
6465
}
6566

6667
export interface ServeBuilderOptions {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json",
3+
"compilerOptions": {
4+
"lib": [
5+
"es2018",
6+
"dom",
7+
"webworker"
8+
],
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/worker",
5+
"lib": [
6+
"es2018",
7+
"webworker"
8+
],
9+
"types": []
10+
},
11+
"include": [
12+
"**/*.worker.ts"
13+
]
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
addEventListener('message', ({ data }) => {
2+
const response = `worker response to ${data}`;
3+
postMessage(response);
4+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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 { JsonParseMode, parseJsonAst, strings, tags } from '@angular-devkit/core';
9+
import {
10+
Rule, SchematicContext, SchematicsException, Tree,
11+
apply, applyTemplates, chain, mergeWith, move, noop, url,
12+
} from '@angular-devkit/schematics';
13+
import { getWorkspace, updateWorkspace } from '../utility/config';
14+
import { appendValueInAstArray, findPropertyInAstObject } from '../utility/json-utils';
15+
import { parseName } from '../utility/parse-name';
16+
import { buildDefaultPath, getProject } from '../utility/project';
17+
import { getProjectTargets } from '../utility/project-targets';
18+
import {
19+
BrowserBuilderOptions,
20+
BrowserBuilderTarget,
21+
WorkspaceSchema,
22+
} from '../utility/workspace-models';
23+
import { Schema as WebWorkerOptions } from './schema';
24+
25+
function getProjectConfiguration(
26+
workspace: WorkspaceSchema,
27+
options: WebWorkerOptions,
28+
): BrowserBuilderOptions {
29+
if (!options.target) {
30+
throw new SchematicsException('Option (target) is required.');
31+
}
32+
33+
const projectTargets = getProjectTargets(workspace, options.project);
34+
if (!projectTargets[options.target]) {
35+
throw new Error(`Target is not defined for this project.`);
36+
}
37+
38+
const target = projectTargets[options.target] as BrowserBuilderTarget;
39+
40+
return target.options;
41+
}
42+
43+
function addConfig(options: WebWorkerOptions, root: string): Rule {
44+
return (host: Tree, context: SchematicContext) => {
45+
context.logger.debug('updating project configuration.');
46+
const workspace = getWorkspace(host);
47+
const config = getProjectConfiguration(workspace, options);
48+
49+
if (config.webWorkerTsConfig) {
50+
// Don't do anything if the configuration is already there.
51+
return;
52+
}
53+
54+
const tsConfigRules = [];
55+
56+
// Add tsconfig.worker.json.
57+
const relativePathToWorkspaceRoot = root.split('/').map(x => '..').join('/');
58+
tsConfigRules.push(mergeWith(apply(url('./files/worker-tsconfig'), [
59+
applyTemplates({ ...options, relativePathToWorkspaceRoot }),
60+
move(root),
61+
])));
62+
63+
// Add build-angular config flag.
64+
config.webWorkerTsConfig = `${root.endsWith('/') ? root : root + '/'}tsconfig.worker.json`;
65+
66+
// Add project tsconfig.json.
67+
// The project level tsconfig.json with webworker lib is for editor support since
68+
// the dom and webworker libs are mutually exclusive.
69+
// Note: this schematic does not change other tsconfigs to use the project-level tsconfig.
70+
const projectTsConfigPath = `${root}/tsconfig.json`;
71+
if (host.exists(projectTsConfigPath)) {
72+
// If the file already exists, alter it.
73+
const buffer = host.read(projectTsConfigPath);
74+
if (buffer) {
75+
const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose);
76+
if (tsCfgAst.kind != 'object') {
77+
throw new SchematicsException('Invalid tsconfig. Was expecting an object');
78+
}
79+
const optsAstNode = findPropertyInAstObject(tsCfgAst, 'compilerOptions');
80+
if (optsAstNode && optsAstNode.kind != 'object') {
81+
throw new SchematicsException(
82+
'Invalid tsconfig "compilerOptions" property; Was expecting an object.');
83+
}
84+
const libAstNode = findPropertyInAstObject(tsCfgAst, 'lib');
85+
if (libAstNode && libAstNode.kind != 'array') {
86+
throw new SchematicsException('Invalid tsconfig "lib" property; expected an array.');
87+
}
88+
const newLibProp = 'webworker';
89+
if (libAstNode && !libAstNode.value.includes(newLibProp)) {
90+
const recorder = host.beginUpdate(projectTsConfigPath);
91+
appendValueInAstArray(recorder, libAstNode, newLibProp);
92+
host.commitUpdate(recorder);
93+
}
94+
}
95+
} else {
96+
// Otherwise create it.
97+
tsConfigRules.push(mergeWith(apply(url('./files/project-tsconfig'), [
98+
applyTemplates({ ...options, relativePathToWorkspaceRoot }),
99+
move(root),
100+
])));
101+
}
102+
103+
// Add worker glob exclusion to tsconfig.app.json.
104+
const workerGlob = '**/*.worker.ts';
105+
const tsConfigPath = config.tsConfig;
106+
const buffer = host.read(tsConfigPath);
107+
if (buffer) {
108+
const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose);
109+
if (tsCfgAst.kind != 'object') {
110+
throw new SchematicsException('Invalid tsconfig. Was expecting an object');
111+
}
112+
const filesAstNode = findPropertyInAstObject(tsCfgAst, 'exclude');
113+
if (filesAstNode && filesAstNode.kind != 'array') {
114+
throw new SchematicsException('Invalid tsconfig "exclude" property; expected an array.');
115+
}
116+
117+
if (filesAstNode && filesAstNode.value.indexOf(workerGlob) == -1) {
118+
const recorder = host.beginUpdate(tsConfigPath);
119+
appendValueInAstArray(recorder, filesAstNode, workerGlob);
120+
host.commitUpdate(recorder);
121+
}
122+
}
123+
124+
return chain([
125+
// Add tsconfigs.
126+
...tsConfigRules,
127+
// Add workspace configuration.
128+
updateWorkspace(workspace),
129+
]);
130+
};
131+
}
132+
133+
function addSnippet(options: WebWorkerOptions): Rule {
134+
return (host: Tree, context: SchematicContext) => {
135+
context.logger.debug('Updating appmodule');
136+
137+
if (options.path === undefined) {
138+
return;
139+
}
140+
141+
const siblingModules = host.getDir(options.path).subfiles
142+
// Find all files that start with the same name, are ts files, and aren't spec files.
143+
.filter(f => f.startsWith(options.name) && f.endsWith('.ts') && !f.endsWith('spec.ts'))
144+
// Sort alphabetically for consistency.
145+
.sort();
146+
147+
if (siblingModules.length === 0) {
148+
// No module to add in.
149+
return;
150+
}
151+
152+
const siblingModulePath = `${options.path}/${siblingModules[0]}`;
153+
const workerCreationSnippet = tags.stripIndent`
154+
if (typeof Worker !== 'undefined') {
155+
// Create a new
156+
const worker = new Worker('./${options.name}.worker', { type: 'module' });
157+
worker.onmessage = ({ data }) => {
158+
console.log('page got message: $\{data\}');
159+
};
160+
worker.postMessage('hello');
161+
} else {
162+
// Web Workers are not supported in this environment.
163+
// You should add a fallback so that your program still executes correctly.
164+
}
165+
`;
166+
167+
// Append the worker creation snippet.
168+
const originalContent = host.read(siblingModulePath);
169+
host.overwrite(siblingModulePath, originalContent + '\n' + workerCreationSnippet);
170+
171+
return host;
172+
};
173+
}
174+
175+
export default function (options: WebWorkerOptions): Rule {
176+
return (host: Tree, context: SchematicContext) => {
177+
const project = getProject(host, options.project);
178+
if (!options.project) {
179+
throw new SchematicsException('Option "project" is required.');
180+
}
181+
if (!project) {
182+
throw new SchematicsException(`Invalid project name (${options.project})`);
183+
}
184+
if (project.projectType !== 'application') {
185+
throw new SchematicsException(`Web Worker requires a project type of "application".`);
186+
}
187+
188+
if (options.path === undefined) {
189+
options.path = buildDefaultPath(project);
190+
}
191+
const parsedPath = parseName(options.path, options.name);
192+
options.name = parsedPath.name;
193+
options.path = parsedPath.path;
194+
const root = project.root || project.sourceRoot || '';
195+
196+
const templateSource = apply(url('./files/worker'), [
197+
applyTemplates({ ...options, ...strings }),
198+
move(parsedPath.path),
199+
]);
200+
201+
return chain([
202+
// Add project configuration.
203+
addConfig(options, root),
204+
// Create the worker in a sibling module.
205+
options.snippet ? addSnippet(options) : noop(),
206+
// Add the worker.
207+
mergeWith(templateSource),
208+
]);
209+
};
210+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
9+
import { Schema as ApplicationOptions } from '../application/schema';
10+
import { Schema as WorkspaceOptions } from '../workspace/schema';
11+
import { Schema as WebWorkerOptions } from './schema';
12+
13+
14+
describe('Service Worker Schematic', () => {
15+
const schematicRunner = new SchematicTestRunner(
16+
'@schematics/angular',
17+
require.resolve('../collection.json'),
18+
);
19+
const defaultOptions: WebWorkerOptions = {
20+
project: 'bar',
21+
target: 'build',
22+
name: 'app',
23+
// path: 'src/app',
24+
snippet: true,
25+
};
26+
27+
let appTree: UnitTestTree;
28+
29+
const workspaceOptions: WorkspaceOptions = {
30+
name: 'workspace',
31+
newProjectRoot: 'projects',
32+
version: '8.0.0',
33+
};
34+
35+
const appOptions: ApplicationOptions = {
36+
name: 'bar',
37+
inlineStyle: false,
38+
inlineTemplate: false,
39+
routing: false,
40+
skipTests: false,
41+
skipPackageJson: false,
42+
};
43+
44+
beforeEach(() => {
45+
appTree = schematicRunner.runSchematic('workspace', workspaceOptions);
46+
appTree = schematicRunner.runSchematic('application', appOptions, appTree);
47+
});
48+
49+
it('should put the worker file in the project root', () => {
50+
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
51+
const path = '/projects/bar/src/app/app.worker.ts';
52+
expect(tree.exists(path)).toEqual(true);
53+
});
54+
55+
it('should put a new tsconfig.json file in the project root', () => {
56+
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
57+
const path = '/projects/bar/tsconfig.json';
58+
expect(tree.exists(path)).toEqual(true);
59+
});
60+
61+
it('should put the tsconfig.worker.json file in the project root', () => {
62+
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
63+
const path = '/projects/bar/tsconfig.worker.json';
64+
expect(tree.exists(path)).toEqual(true);
65+
});
66+
67+
it('should add the webWorkerTsConfig option to workspace', () => {
68+
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
69+
const { projects } = JSON.parse(tree.readContent('/angular.json'));
70+
expect(projects.bar.architect.build.options.webWorkerTsConfig)
71+
.toBe('projects/bar/tsconfig.worker.json');
72+
});
73+
74+
it('should add exclusions to tsconfig.app.json', () => {
75+
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
76+
const { exclude } = JSON.parse(tree.readContent('/projects/bar/tsconfig.app.json'));
77+
expect(exclude).toContain('**/*.worker.ts');
78+
});
79+
80+
it('should add snippet to sibling file', () => {
81+
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
82+
const appComponent = tree.readContent('/projects/bar/src/app/app.component.ts');
83+
expect(appComponent).toContain(`new Worker('./${defaultOptions.name}.worker`);
84+
});
85+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"id": "SchematicsAngularWebWorker",
4+
"title": "Angular Web Worker Options Schema",
5+
"type": "object",
6+
"description": "Pass this schematic to the \"run\" command to create a Web Worker",
7+
"properties": {
8+
"path": {
9+
"type": "string",
10+
"format": "path",
11+
"description": "The path at which to create the worker file, relative to the current workspace.",
12+
"visible": false
13+
},
14+
"project": {
15+
"type": "string",
16+
"description": "The name of the project.",
17+
"$default": {
18+
"$source": "projectName"
19+
}
20+
},
21+
"target": {
22+
"type": "string",
23+
"description": "The target to apply service worker to.",
24+
"default": "build"
25+
},
26+
"name": {
27+
"type": "string",
28+
"description": "The name of the worker.",
29+
"$default": {
30+
"$source": "argv",
31+
"index": 0
32+
},
33+
"x-prompt": "What name would you like to use for the worker?"
34+
},
35+
"snippet": {
36+
"type": "boolean",
37+
"default": true,
38+
"description": "Add a worker creation snippet in a sibling file of the same name."
39+
}
40+
},
41+
"required": [
42+
"name",
43+
"project"
44+
]
45+
}

0 commit comments

Comments
 (0)