refactor(elements): ng-add schematics (#33723)

This PR brings a couple of changes;
- Removes undeed dependencies in bazel targets such as `//packages/common` & `//packages/core`
- Removes RxJs usage
- Adds `document-register-element` to architect test targets
- Use @schematics/angular helpers
- Uses the standard `$source": "projectName"` to get the projectName, which is defined in the `schema.json`
- Use workspace writer to update the workspace config

PR Close #33723
This commit is contained in:
Alan Agius 2019-11-11 09:27:36 +01:00 committed by Matias Niemelä
parent de043a4bcb
commit 588823b437
5 changed files with 100 additions and 73 deletions

View File

@ -3,7 +3,8 @@
"schematics": { "schematics": {
"ng-add": { "ng-add": {
"description": "Adds the document-register-element polyfill.", "description": "Adds the document-register-element polyfill.",
"factory": "./ng-add" "factory": "./ng-add",
"schema": "ng-add/schema.json"
} }
} }
} }

View File

@ -10,10 +10,8 @@ ts_library(
"schema.ts", "schema.ts",
], ],
deps = [ deps = [
"//packages/common",
"//packages/core",
"@npm//@angular-devkit/schematics", "@npm//@angular-devkit/schematics",
"@npm//rxjs", "@npm//@schematics/angular",
], ],
) )
@ -23,12 +21,13 @@ ts_library(
srcs = [ srcs = [
"index_spec.ts", "index_spec.ts",
], ],
data = [
"schema.json",
],
deps = [ deps = [
":ng-add", ":ng-add",
"//packages/common",
"//packages/core",
"@npm//@angular-devkit/schematics", "@npm//@angular-devkit/schematics",
"@npm//rxjs", "@npm//@schematics/angular",
], ],
) )
@ -37,6 +36,5 @@ jasmine_node_test(
deps = [ deps = [
":test_lib", ":test_lib",
"//packages/elements/schematics:collection", "//packages/elements/schematics:collection",
"@npm//@schematics/angular",
], ],
) )

View File

@ -5,72 +5,74 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {Rule, SchematicContext, Tree, chain, noop} from '@angular-devkit/schematics'; import {Rule, SchematicContext, SchematicsException, Tree, chain, noop} from '@angular-devkit/schematics';
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks'; import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
import {NodeDependencyType, addPackageJsonDependency} from '@schematics/angular/utility/dependencies';
import {getWorkspace} from '@schematics/angular/utility/workspace';
import {Schema} from './schema'; import {Schema} from './schema';
export default function(options: Schema): Rule { export default function(options: Schema): Rule {
return chain([ return chain([
options && options.skipPackageJson ? noop() : addPackageJsonDependency(), addScript(options) options && options.skipPackageJson ? noop() : addPolyfillDependency(),
addPolyfill(options),
]); ]);
} }
/** Adds a package.json dependency for document-register-element */ /** Adds a package.json dependency for document-register-element */
function addPackageJsonDependency() { function addPolyfillDependency(): Rule {
return (host: Tree, context: SchematicContext) => { return (host: Tree, context: SchematicContext) => {
addPackageJsonDependency(host, {
if (host.exists('package.json')) { type: NodeDependencyType.Default,
const jsonStr = host.read('package.json') !.toString('utf-8'); name: 'document-register-element',
const json = JSON.parse(jsonStr); version: '^1.7.2',
});
// If there are no dependencies, create an entry for dependencies. context.logger.info('Added "document-register-element" as a dependency.');
const type = 'dependencies';
if (!json[type]) {
json[type] = {};
}
// If not already present, add the dependency.
const pkg = 'document-register-element';
const version = '^1.7.2';
if (!json[type][pkg]) {
json[type][pkg] = version;
}
// Write the JSON back to package.json
host.overwrite('package.json', JSON.stringify(json, null, 2));
context.logger.log('info', 'Added `document-register-element` as a dependency.');
// Install the dependency // Install the dependency
context.addTask(new NodePackageInstallTask()); context.addTask(new NodePackageInstallTask());
}
return host;
}; };
} }
/** Adds the document-register-element.js script to the angular CLI json. */ /** Adds the document-register-element.js to the polyfills file. */
function addScript(options: Schema) { function addPolyfill(options: Schema): Rule {
return (host: Tree, context: SchematicContext) => { return async(host: Tree, context: SchematicContext) => {
const script = 'node_modules/document-register-element/build/document-register-element.js'; const projectName = options.project;
if (!projectName) {
try { throw new SchematicsException('Option "project" is required.');
// Handle the new json - angular.json
const angularJsonFile = host.read('angular.json');
if (angularJsonFile) {
const json = JSON.parse(angularJsonFile.toString('utf-8'));
const project = Object.keys(json['projects'])[0] || options.project;
const scripts = json['projects'][project]['architect']['build']['options']['scripts'];
scripts.push({input: script});
host.overwrite('angular.json', JSON.stringify(json, null, 2));
}
} catch {
context.logger.log(
'warn', 'Failed to add the polyfill document-register-element.js to scripts');
} }
context.logger.log('info', 'Added document-register-element.js polyfill to scripts'); const workspace = await getWorkspace(host);
const project = workspace.projects.get(projectName);
return host; if (!project) {
throw new SchematicsException(`Project ${projectName} is not defined in this workspace.`);
}
if (project.extensions['projectType'] !== 'application') {
throw new SchematicsException(
`@angular/elements requires a project type of "application" but ${projectName} isn't.`);
}
const buildTarget = project.targets.get('build');
if (!buildTarget || !buildTarget.options) {
throw new SchematicsException(`Cannot find 'options' for ${projectName} build target.`);
}
const {polyfills} = buildTarget.options;
if (typeof polyfills !== 'string') {
throw new SchematicsException(`polyfills for ${projectName} build target is not a string.`);
}
const content = host.read(polyfills).toString();
if (!content.includes('document-register-element')) {
// Add string at the end of the file.
const recorder = host.beginUpdate(polyfills);
recorder.insertRight(content.length, `import 'document-register-element';\n`);
host.commitUpdate(recorder);
}
context.logger.info('Added "document-register-element" to polyfills.');
}; };
} }

View File

@ -7,19 +7,14 @@
*/ */
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
import * as path from 'path'; import * as path from 'path';
import {Observable} from 'rxjs';
import {concatMap} from 'rxjs/operators';
import {Schema as ElementsOptions} from './schema'; import {Schema as ElementsOptions} from './schema';
const polyfillPath = 'node_modules/document-register-element/build/document-register-element.js';
// tslint:disable:max-line-length // tslint:disable:max-line-length
describe('Elements Schematics', () => { describe('Elements Schematics', () => {
const schematicRunner = new SchematicTestRunner( const schematicRunner = new SchematicTestRunner(
'@angular/elements', path.join(__dirname, '../test-collection.json'), ); '@angular/elements', path.join(__dirname, '../test-collection.json'), );
const defaultOptions: ElementsOptions = {project: 'bar', skipPackageJson: false}; const defaultOptions: ElementsOptions = {project: 'elements', skipPackageJson: false};
let appTree: UnitTestTree; let appTree: UnitTestTree;
@ -40,20 +35,29 @@ describe('Elements Schematics', () => {
skipTests: false, skipTests: false,
}; };
beforeEach((done) => { beforeEach(async() => {
schematicRunner.runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions) appTree = await schematicRunner
.pipe(concatMap( .runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions)
(tree) => schematicRunner.runExternalSchematicAsync( .toPromise();
'@schematics/angular', 'application', appOptions, tree))) appTree =
.subscribe((tree: UnitTestTree) => appTree = tree, done.fail, done); await schematicRunner
.runExternalSchematicAsync('@schematics/angular', 'application', appOptions, appTree)
.toPromise();
}); });
it('should run the ng-add schematic', async() => { it('should run the ng-add schematic', async() => {
const tree = const tree =
await schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise(); await schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise();
const configText = tree.readContent('/angular.json'); expect(tree.readContent('/projects/elements/src/polyfills.ts'))
const config = JSON.parse(configText); .toContain(`import 'document-register-element';`);
const scripts = config.projects.elements.architect.build.options.scripts; });
expect(scripts[0].input).toEqual(polyfillPath);
it('should add polyfill as a dependency in package.json', async() => {
const tree =
await schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise();
const pkgJsonText = tree.readContent('/package.json');
const pkgJson = JSON.parse(pkgJsonText);
const {dependencies} = pkgJson;
expect(dependencies['document-register-element']).toBeDefined();
}); });
}); });

View File

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsAngularElementsNgAdd",
"title": "Angular Elements Ng Add Schema",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project.",
"$default": {
"$source": "projectName"
}
},
"skipPackageJson": {
"description": "When true, does not add dependencies to the \"package.json\" file.",
"type": "boolean",
"default": false
}
},
"required": [
]
}