Skip to content

Commit b35f1ad

Browse files
committed
feat(@schematics/angular): add web worker schematics
1 parent cac5d98 commit b35f1ad

File tree

9 files changed

+365
-68
lines changed

9 files changed

+365
-68
lines changed

Diff for: 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
}

Diff for: 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+
experimentalWebWorkerTsConfig?: string;
6465
}
6566

6667
export interface ServeBuilderOptions {
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+
});

Diff for: packages/schematics/angular/web-worker/index.ts

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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 { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
14+
import { getWorkspace, updateWorkspace } from '../utility/config';
15+
import { appendValueInAstArray, findPropertyInAstObject } from '../utility/json-utils';
16+
import { parseName } from '../utility/parse-name';
17+
import { buildDefaultPath, getProject } from '../utility/project';
18+
import { getProjectTargets } from '../utility/project-targets';
19+
import {
20+
BrowserBuilderOptions,
21+
BrowserBuilderTarget,
22+
WorkspaceSchema,
23+
} from '../utility/workspace-models';
24+
import { Schema as WebWorkerOptions } from './schema';
25+
26+
function getProjectConfiguration(
27+
workspace: WorkspaceSchema,
28+
options: WebWorkerOptions,
29+
): BrowserBuilderOptions {
30+
const projectTargets = getProjectTargets(workspace, options.project);
31+
if (!projectTargets[options.target]) {
32+
throw new Error(`Target is not defined for this project.`);
33+
}
34+
35+
const target = projectTargets[options.target] as BrowserBuilderTarget;
36+
37+
return target.options;
38+
}
39+
40+
function addConfig(options: WebWorkerOptions, root: string): Rule {
41+
return (host: Tree, context: SchematicContext) => {
42+
context.logger.debug('updating project configuration.');
43+
const workspace = getWorkspace(host);
44+
const config = getProjectConfiguration(workspace, options);
45+
46+
if (!!config.experimentalWebWorkerTsConfig) {
47+
// Don't do anything if the configuration is already there.
48+
return;
49+
}
50+
51+
config.experimentalWebWorkerTsConfig =
52+
`${root.endsWith('/') ? root : root + '/'}tsconfig.worker.json`;
53+
54+
const relativePathToWorkspaceRoot = root.split('/').map(x => '..').join('/');
55+
const templateSource = apply(url('./other-files'), [
56+
applyTemplates({ ...options, relativePathToWorkspaceRoot }),
57+
move(root),
58+
]);
59+
60+
// Add worker glob exclusion to tsconfig.app.json.
61+
const workerGlob = '**/*.worker.ts';
62+
const tsConfigPath = config.tsConfig;
63+
const buffer = host.read(tsConfigPath);
64+
if (buffer) {
65+
const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose);
66+
if (tsCfgAst.kind != 'object') {
67+
throw new SchematicsException('Invalid tsconfig. Was expecting an object');
68+
}
69+
const filesAstNode = findPropertyInAstObject(tsCfgAst, 'exclude');
70+
if (filesAstNode && filesAstNode.kind != 'array') {
71+
throw new SchematicsException('Invalid tsconfig "exclude" property; expected an array.');
72+
}
73+
const recorder = host.beginUpdate(tsConfigPath);
74+
if (!filesAstNode) {
75+
// Do nothing if the files array does not exist. This means exclude or include are
76+
// set and we shouldn't mess with that.
77+
} else {
78+
if (filesAstNode.value.indexOf(workerGlob) == -1) {
79+
appendValueInAstArray(recorder, filesAstNode, workerGlob);
80+
}
81+
}
82+
83+
host.commitUpdate(recorder);
84+
}
85+
86+
return chain([
87+
// Add tsconfig.worker.json.
88+
mergeWith(templateSource),
89+
// Add workspace configuration.
90+
updateWorkspace(workspace),
91+
]);
92+
};
93+
}
94+
95+
function addSnippet(options: WebWorkerOptions): Rule {
96+
return (host: Tree, context: SchematicContext) => {
97+
context.logger.debug('Updating appmodule');
98+
99+
if (options.path === undefined) {
100+
return;
101+
}
102+
103+
const siblingModules = host.getDir(options.path).subfiles
104+
// Find all files that start with the same name, are ts files, and aren't spec files.
105+
.filter(f => f.startsWith(options.name) && f.endsWith('.ts') && !f.endsWith('spec.ts'))
106+
// Sort alphabetically for consistency.
107+
.sort();
108+
109+
if (siblingModules.length === 0) {
110+
// No module to add in.
111+
return;
112+
}
113+
114+
const siblingModulePath = `${options.path}/${siblingModules[0]}`;
115+
const workerCreationSnippet = tags.stripIndent`
116+
const worker = new Worker('./${options.name}.worker', { type: 'module' });
117+
worker.onmessage = ({ data }) => {
118+
console.log('page got message: \${data}');
119+
};
120+
worker.postMessage('hello');
121+
`;
122+
123+
// Append the worker creation snippet.
124+
const originalContent = host.read(siblingModulePath);
125+
host.overwrite(siblingModulePath, originalContent + '\n' + workerCreationSnippet);
126+
127+
return host;
128+
};
129+
}
130+
131+
export default function (options: WebWorkerOptions): Rule {
132+
return (host: Tree, context: SchematicContext) => {
133+
const project = getProject(host, options.project);
134+
if (!options.project) {
135+
throw new SchematicsException('Option "project" is required.');
136+
}
137+
if (!project) {
138+
throw new SchematicsException(`Invalid project name (${options.project})`);
139+
}
140+
if (project.projectType !== 'application') {
141+
throw new SchematicsException(`Web Worker requires a project type of "application".`);
142+
}
143+
144+
if (options.path === undefined) {
145+
options.path = buildDefaultPath(project);
146+
}
147+
const parsedPath = parseName(options.path, options.name);
148+
options.name = parsedPath.name;
149+
options.path = parsedPath.path;
150+
const root = project.root || project.sourceRoot || '';
151+
152+
const templateSource = apply(url('./files'), [
153+
applyTemplates({ ...options, ...strings }),
154+
move(parsedPath.path),
155+
]);
156+
157+
context.addTask(new NodePackageInstallTask());
158+
159+
return chain([
160+
// Add project configuration.
161+
addConfig(options, root),
162+
// Create the worker in a sibling module.
163+
options.snippet ? addSnippet(options) : noop(),
164+
// Add the worker.
165+
mergeWith(templateSource),
166+
]);
167+
};
168+
}

Diff for: packages/schematics/angular/web-worker/index_spec.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 the tsconfig.worker.json file in the project root', () => {
56+
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
57+
const path = '/projects/bar/tsconfig.worker.json';
58+
expect(tree.exists(path)).toEqual(true);
59+
60+
const { projects } = JSON.parse(tree.readContent('/angular.json'));
61+
expect(projects.bar.architect.build.options.experimentalWebWorkerTsConfig)
62+
.toBe('projects/bar/tsconfig.worker.json');
63+
});
64+
65+
it('should add exclusions to tsconfig.app.json', () => {
66+
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
67+
const { exclude } = JSON.parse(tree.readContent('/projects/bar/tsconfig.app.json'));
68+
expect(exclude).toContain('**/*.worker.ts');
69+
});
70+
71+
it('should add snippet to sibling file', () => {
72+
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
73+
const appComponent = tree.readContent('/projects/bar/src/app/app.component.ts');
74+
expect(appComponent).toContain(`new Worker('./${defaultOptions.name}.worker`);
75+
});
76+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "<%= relativePathToWorkspaceRoot %>/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+
}

Diff for: packages/schematics/angular/web-worker/schema.d.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
9+
export interface Schema {
10+
/**
11+
* The path at which to create the worker file, relative to the current workspace.
12+
*/
13+
path?: string;
14+
/**
15+
* The name of the project.
16+
*/
17+
project: string;
18+
/**
19+
* The target to apply service worker to.
20+
*/
21+
target: string;
22+
/**
23+
* The name of the worker..
24+
*/
25+
name: string;
26+
/**
27+
* Add a worker creation snippet in a sibling file of the same name.
28+
*/
29+
snippet: boolean;
30+
}

Diff for: packages/schematics/angular/web-worker/schema.json

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
]
44+
}

0 commit comments

Comments
 (0)