Skip to content

Commit

Permalink
feat(@schematics/angular): add web worker schematics
Browse files Browse the repository at this point in the history
  • Loading branch information
filipesilva authored and mgechev committed Apr 2, 2019
1 parent ed0e6aa commit 7ed4a60
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 87 deletions.
6 changes: 6 additions & 0 deletions packages/schematics/angular/collection.json
Expand Up @@ -100,6 +100,12 @@
"factory": "./library",
"schema": "./library/schema.json",
"description": "Generate a library project for Angular."
},
"webWorker": {
"aliases": ["web-worker", "worker"],
"factory": "./web-worker",
"schema": "./web-worker/schema.json",
"description": "Create a Web Worker ."
}
}
}
1 change: 1 addition & 0 deletions packages/schematics/angular/utility/workspace-models.ts
Expand Up @@ -61,6 +61,7 @@ export interface BrowserBuilderOptions extends BrowserBuilderBaseOptions {
maximumError?: string;
}[];
es5BrowserSupport?: boolean;
webWorkerTsConfig?: string;
}

export interface ServeBuilderOptions {
Expand Down
@@ -0,0 +1,10 @@
{
"extends": "<%= relativePathToWorkspaceRoot %>/tsconfig.json",
"compilerOptions": {
"lib": [
"es2018",
"dom",
"webworker"
],
}
}
@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/worker",
"lib": [
"es2018",
"webworker"
],
"types": []
},
"include": [
"**/*.worker.ts"
]
}
@@ -0,0 +1,4 @@
addEventListener('message', ({ data }) => {
const response = `worker response to ${data}`;
postMessage(response);
});
210 changes: 210 additions & 0 deletions packages/schematics/angular/web-worker/index.ts
@@ -0,0 +1,210 @@
/**
* @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 { JsonParseMode, parseJsonAst, strings, tags } from '@angular-devkit/core';
import {
Rule, SchematicContext, SchematicsException, Tree,
apply, applyTemplates, chain, mergeWith, move, noop, url,
} from '@angular-devkit/schematics';
import { getWorkspace, updateWorkspace } from '../utility/config';
import { appendValueInAstArray, findPropertyInAstObject } from '../utility/json-utils';
import { parseName } from '../utility/parse-name';
import { buildDefaultPath, getProject } from '../utility/project';
import { getProjectTargets } from '../utility/project-targets';
import {
BrowserBuilderOptions,
BrowserBuilderTarget,
WorkspaceSchema,
} from '../utility/workspace-models';
import { Schema as WebWorkerOptions } from './schema';

function getProjectConfiguration(
workspace: WorkspaceSchema,
options: WebWorkerOptions,
): BrowserBuilderOptions {
if (!options.target) {
throw new SchematicsException('Option (target) is required.');
}

const projectTargets = getProjectTargets(workspace, options.project);
if (!projectTargets[options.target]) {
throw new Error(`Target is not defined for this project.`);
}

const target = projectTargets[options.target] as BrowserBuilderTarget;

return target.options;
}

function addConfig(options: WebWorkerOptions, root: string): Rule {
return (host: Tree, context: SchematicContext) => {
context.logger.debug('updating project configuration.');
const workspace = getWorkspace(host);
const config = getProjectConfiguration(workspace, options);

if (config.webWorkerTsConfig) {
// Don't do anything if the configuration is already there.
return;
}

const tsConfigRules = [];

// Add tsconfig.worker.json.
const relativePathToWorkspaceRoot = root.split('/').map(x => '..').join('/');
tsConfigRules.push(mergeWith(apply(url('./files/worker-tsconfig'), [
applyTemplates({ ...options, relativePathToWorkspaceRoot }),
move(root),
])));

// Add build-angular config flag.
config.webWorkerTsConfig = `${root.endsWith('/') ? root : root + '/'}tsconfig.worker.json`;

// Add project tsconfig.json.
// The project level tsconfig.json with webworker lib is for editor support since
// the dom and webworker libs are mutually exclusive.
// Note: this schematic does not change other tsconfigs to use the project-level tsconfig.
const projectTsConfigPath = `${root}/tsconfig.json`;
if (host.exists(projectTsConfigPath)) {
// If the file already exists, alter it.
const buffer = host.read(projectTsConfigPath);
if (buffer) {
const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose);
if (tsCfgAst.kind != 'object') {
throw new SchematicsException('Invalid tsconfig. Was expecting an object');
}
const optsAstNode = findPropertyInAstObject(tsCfgAst, 'compilerOptions');
if (optsAstNode && optsAstNode.kind != 'object') {
throw new SchematicsException(
'Invalid tsconfig "compilerOptions" property; Was expecting an object.');
}
const libAstNode = findPropertyInAstObject(tsCfgAst, 'lib');
if (libAstNode && libAstNode.kind != 'array') {
throw new SchematicsException('Invalid tsconfig "lib" property; expected an array.');
}
const newLibProp = 'webworker';
if (libAstNode && !libAstNode.value.includes(newLibProp)) {
const recorder = host.beginUpdate(projectTsConfigPath);
appendValueInAstArray(recorder, libAstNode, newLibProp);
host.commitUpdate(recorder);
}
}
} else {
// Otherwise create it.
tsConfigRules.push(mergeWith(apply(url('./files/project-tsconfig'), [
applyTemplates({ ...options, relativePathToWorkspaceRoot }),
move(root),
])));
}

// Add worker glob exclusion to tsconfig.app.json.
const workerGlob = '**/*.worker.ts';
const tsConfigPath = config.tsConfig;
const buffer = host.read(tsConfigPath);
if (buffer) {
const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose);
if (tsCfgAst.kind != 'object') {
throw new SchematicsException('Invalid tsconfig. Was expecting an object');
}
const filesAstNode = findPropertyInAstObject(tsCfgAst, 'exclude');
if (filesAstNode && filesAstNode.kind != 'array') {
throw new SchematicsException('Invalid tsconfig "exclude" property; expected an array.');
}

if (filesAstNode && filesAstNode.value.indexOf(workerGlob) == -1) {
const recorder = host.beginUpdate(tsConfigPath);
appendValueInAstArray(recorder, filesAstNode, workerGlob);
host.commitUpdate(recorder);
}
}

return chain([
// Add tsconfigs.
...tsConfigRules,
// Add workspace configuration.
updateWorkspace(workspace),
]);
};
}

function addSnippet(options: WebWorkerOptions): Rule {
return (host: Tree, context: SchematicContext) => {
context.logger.debug('Updating appmodule');

if (options.path === undefined) {
return;
}

const siblingModules = host.getDir(options.path).subfiles
// Find all files that start with the same name, are ts files, and aren't spec files.
.filter(f => f.startsWith(options.name) && f.endsWith('.ts') && !f.endsWith('spec.ts'))
// Sort alphabetically for consistency.
.sort();

if (siblingModules.length === 0) {
// No module to add in.
return;
}

const siblingModulePath = `${options.path}/${siblingModules[0]}`;
const workerCreationSnippet = tags.stripIndent`
if (typeof Worker !== 'undefined') {
// Create a new
const worker = new Worker('./${options.name}.worker', { type: 'module' });
worker.onmessage = ({ data }) => {
console.log('page got message: $\{data\}');
};
worker.postMessage('hello');
} else {
// Web Workers are not supported in this environment.
// You should add a fallback so that your program still executes correctly.
}
`;

// Append the worker creation snippet.
const originalContent = host.read(siblingModulePath);
host.overwrite(siblingModulePath, originalContent + '\n' + workerCreationSnippet);

return host;
};
}

export default function (options: WebWorkerOptions): Rule {
return (host: Tree, context: SchematicContext) => {
const project = getProject(host, options.project);
if (!options.project) {
throw new SchematicsException('Option "project" is required.');
}
if (!project) {
throw new SchematicsException(`Invalid project name (${options.project})`);
}
if (project.projectType !== 'application') {
throw new SchematicsException(`Web Worker requires a project type of "application".`);
}

if (options.path === undefined) {
options.path = buildDefaultPath(project);
}
const parsedPath = parseName(options.path, options.name);
options.name = parsedPath.name;
options.path = parsedPath.path;
const root = project.root || project.sourceRoot || '';

const templateSource = apply(url('./files/worker'), [
applyTemplates({ ...options, ...strings }),
move(parsedPath.path),
]);

return chain([
// Add project configuration.
addConfig(options, root),
// Create the worker in a sibling module.
options.snippet ? addSnippet(options) : noop(),
// Add the worker.
mergeWith(templateSource),
]);
};
}
85 changes: 85 additions & 0 deletions packages/schematics/angular/web-worker/index_spec.ts
@@ -0,0 +1,85 @@
/**
* @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 { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { Schema as ApplicationOptions } from '../application/schema';
import { Schema as WorkspaceOptions } from '../workspace/schema';
import { Schema as WebWorkerOptions } from './schema';


describe('Service Worker Schematic', () => {
const schematicRunner = new SchematicTestRunner(
'@schematics/angular',
require.resolve('../collection.json'),
);
const defaultOptions: WebWorkerOptions = {
project: 'bar',
target: 'build',
name: 'app',
// path: 'src/app',
snippet: true,
};

let appTree: UnitTestTree;

const workspaceOptions: WorkspaceOptions = {
name: 'workspace',
newProjectRoot: 'projects',
version: '8.0.0',
};

const appOptions: ApplicationOptions = {
name: 'bar',
inlineStyle: false,
inlineTemplate: false,
routing: false,
skipTests: false,
skipPackageJson: false,
};

beforeEach(() => {
appTree = schematicRunner.runSchematic('workspace', workspaceOptions);
appTree = schematicRunner.runSchematic('application', appOptions, appTree);
});

it('should put the worker file in the project root', () => {
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
const path = '/projects/bar/src/app/app.worker.ts';
expect(tree.exists(path)).toEqual(true);
});

it('should put a new tsconfig.json file in the project root', () => {
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
const path = '/projects/bar/tsconfig.json';
expect(tree.exists(path)).toEqual(true);
});

it('should put the tsconfig.worker.json file in the project root', () => {
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
const path = '/projects/bar/tsconfig.worker.json';
expect(tree.exists(path)).toEqual(true);
});

it('should add the webWorkerTsConfig option to workspace', () => {
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
const { projects } = JSON.parse(tree.readContent('/angular.json'));
expect(projects.bar.architect.build.options.webWorkerTsConfig)
.toBe('projects/bar/tsconfig.worker.json');
});

it('should add exclusions to tsconfig.app.json', () => {
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
const { exclude } = JSON.parse(tree.readContent('/projects/bar/tsconfig.app.json'));
expect(exclude).toContain('**/*.worker.ts');
});

it('should add snippet to sibling file', () => {
const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree);
const appComponent = tree.readContent('/projects/bar/src/app/app.component.ts');
expect(appComponent).toContain(`new Worker('./${defaultOptions.name}.worker`);
});
});
45 changes: 45 additions & 0 deletions packages/schematics/angular/web-worker/schema.json
@@ -0,0 +1,45 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsAngularWebWorker",
"title": "Angular Web Worker Options Schema",
"type": "object",
"description": "Pass this schematic to the \"run\" command to create a Web Worker",
"properties": {
"path": {
"type": "string",
"format": "path",
"description": "The path at which to create the worker file, relative to the current workspace.",
"visible": false
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": {
"$source": "projectName"
}
},
"target": {
"type": "string",
"description": "The target to apply service worker to.",
"default": "build"
},
"name": {
"type": "string",
"description": "The name of the worker.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the worker?"
},
"snippet": {
"type": "boolean",
"default": true,
"description": "Add a worker creation snippet in a sibling file of the same name."
}
},
"required": [
"name",
"project"
]
}

0 comments on commit 7ed4a60

Please sign in to comment.