refactor(bazel): Create ng-add schematic for Bazel (#28436)

The logic to create additional files needed for Bazel are currently
hosted in `ng new`. Such files include the main.*.ts files needed
for AOT and a different angular.json to use Bazel builder, among others.

This commit refactors the logic into `ng add` so that it can be used to
perform the same modifications in an existing project. Users could do so
by running `ng add @angular/bazel`.

With this change, `ng new` effectively becomes an orchestrator that runs
the original `ng new` followed by `ng add @angular/bazel`.

PR Close #28436
This commit is contained in:
Keen Yee Liau
2019-01-28 11:03:04 -08:00
committed by Matias Niemelä
parent 744b0205e2
commit 4a92fa9471
20 changed files with 617 additions and 542 deletions

View File

@ -23,6 +23,7 @@ npm_package(
"//packages/bazel/src/ngc-wrapped:ngc_lib",
"//packages/bazel/src/protractor/utils",
"//packages/bazel/src/schematics/bazel-workspace",
"//packages/bazel/src/schematics/ng-add",
"//packages/bazel/src/schematics/ng-new",
],
)

View File

@ -15,6 +15,7 @@ jasmine_node_test(
bootstrap = ["angular/tools/testing/init_node_spec.js"],
deps = [
"//packages/bazel/src/schematics/bazel-workspace:test",
"//packages/bazel/src/schematics/ng-add:test",
"//packages/bazel/src/schematics/ng-new:test",
"//tools/testing:node",
],

View File

@ -86,6 +86,10 @@ export default function(options: BazelWorkspaceOptions): Rule {
}
});
if (!host.exists('yarn.lock')) {
host.create('yarn.lock', '');
}
const workspaceVersions = {
'RULES_NODEJS_VERSION': '0.16.5',
'RULES_TYPESCRIPT_VERSION': '0.22.1',

View File

@ -29,17 +29,19 @@ describe('Bazel-workspace Schematic', () => {
expect(files).toContain('/yarn.lock');
});
it('should find existing Angular version', () => {
let host = new UnitTestTree(new HostTree);
host.create('/node_modules/@angular/core/package.json', JSON.stringify({
name: '@angular/core',
version: '6.6.6',
}));
const options = {...defaultOptions};
host = schematicRunner.runSchematic('bazel-workspace', options, host);
expect(host.files).toContain('/WORKSPACE');
const workspace = host.readContent('/WORKSPACE');
expect(workspace).toMatch('ANGULAR_VERSION = "6.6.6"');
it('should generate empty yarn.lock file', () => {
const host = schematicRunner.runSchematic('bazel-workspace', defaultOptions);
expect(host.files).toContain('/yarn.lock');
expect(host.readContent('/yarn.lock')).toBe('');
});
it('should not replace yarn.lock if it exists', () => {
let host = new UnitTestTree(new HostTree());
host.create('yarn.lock', 'some content');
expect(host.files).toContain('/yarn.lock');
host = schematicRunner.runSchematic('bazel-workspace', defaultOptions, host);
expect(host.files).toContain('/yarn.lock');
expect(host.readContent('/yarn.lock')).toBe('some content');
});
it('should have the correct entry_module for devserver', () => {

View File

@ -1,17 +1,22 @@
{
"name": "@angular/bazel",
"version": "0.1",
"schematics": {
"ng-new": {
"factory": "./ng-new",
"schema": "./ng-new/schema.json",
"description": "Create an Angular project that builds with Bazel."
},
"bazel-workspace": {
"factory": "./bazel-workspace",
"schema": "./bazel-workspace/schema.json",
"description": "Setup Bazel workspace",
"hidden": true
}
"name": "@angular/bazel",
"version": "0.1",
"schematics": {
"ng-add": {
"factory": "./ng-add",
"schema": "ng-add/schema.json",
"description": "Add Bazel build files and configurations to a project"
},
"ng-new": {
"factory": "./ng-new",
"schema": "./ng-new/schema.json",
"description": "Create an Angular project that builds with Bazel."
},
"bazel-workspace": {
"factory": "./bazel-workspace",
"schema": "./bazel-workspace/schema.json",
"description": "Setup Bazel workspace",
"hidden": true
}
}
}

View File

@ -0,0 +1,36 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "ng-add",
srcs = [
"index.ts",
"schema.d.ts",
],
data = glob(["files/**/*"]) + [
"schema.json",
],
deps = [
"//packages/bazel/src/schematics/bazel-workspace",
"@ngdeps//@angular-devkit/core",
"@ngdeps//@angular-devkit/schematics",
"@ngdeps//@schematics/angular",
"@ngdeps//typescript",
],
)
ts_library(
name = "test",
testonly = True,
srcs = [
"index_spec.ts",
],
data = [
"//packages/bazel/src/schematics:package_assets",
],
deps = [
":ng-add",
"@ngdeps//@angular-devkit/schematics",
],
)

View File

@ -0,0 +1,232 @@
/**
* @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
*
* @fileoverview Schematics for ng-new project that builds with Bazel.
*/
import {SchematicContext, apply, applyTemplates, chain, mergeWith, move, Rule, schematic, Tree, url, SchematicsException, UpdateRecorder,} from '@angular-devkit/schematics';
import {parseJsonAst, JsonAstObject, strings, JsonValue} from '@angular-devkit/core';
import {findPropertyInAstObject, insertPropertyInAstObjectInOrder} from '@schematics/angular/utility/json-utils';
import {validateProjectName} from '@schematics/angular/utility/validation';
import {getWorkspacePath} from '@schematics/angular/utility/config';
import {Schema} from './schema';
/**
* Packages that build under Bazel require additional dev dependencies. This
* function adds those dependencies to "devDependencies" section in
* package.json.
*/
function addDevDependenciesToPackageJson(options: Schema) {
return (host: Tree) => {
const packageJson = 'package.json';
if (!host.exists(packageJson)) {
throw new Error(`Could not find ${packageJson}`);
}
const packageJsonContent = host.read(packageJson);
if (!packageJsonContent) {
throw new Error('Failed to read package.json content');
}
const jsonAst = parseJsonAst(packageJsonContent.toString()) as JsonAstObject;
const deps = findPropertyInAstObject(jsonAst, 'dependencies') as JsonAstObject;
const devDeps = findPropertyInAstObject(jsonAst, 'devDependencies') as JsonAstObject;
const angularCoreNode = findPropertyInAstObject(deps, '@angular/core');
if (!angularCoreNode) {
throw new Error('@angular/core dependency not found in package.json');
}
const angularCoreVersion = angularCoreNode.value as string;
const devDependencies: {[k: string]: string} = {
'@angular/bazel': angularCoreVersion,
// TODO(kyliau): Consider moving this to latest-versions.ts
'@bazel/bazel': '^0.22.1',
'@bazel/ibazel': '^0.9.0',
'@bazel/karma': '^0.22.1',
'@bazel/typescript': '^0.22.1',
};
const recorder = host.beginUpdate(packageJson);
for (const packageName of Object.keys(devDependencies)) {
const version = devDependencies[packageName];
const indent = 4;
insertPropertyInAstObjectInOrder(recorder, devDeps, packageName, version, indent);
}
host.commitUpdate(recorder);
return host;
};
}
/**
* Append main.dev.ts and main.prod.ts to src directory. These files are needed
* by Bazel for devserver and prodserver, respectively. They are different from
* main.ts generated by CLI because they use platformBrowser (AOT) instead of
* platformBrowserDynamic (JIT).
*/
function addDevAndProdMainForAot(options: Schema) {
return (host: Tree) => {
return mergeWith(apply(url('./files'), [
applyTemplates({
utils: strings,
...options,
'dot': '.',
}),
move('/src'),
]));
};
}
/**
* Append '/bazel-out' to the gitignore file.
*/
function updateGitignore() {
return (host: Tree) => {
const gitignore = '/.gitignore';
if (!host.exists(gitignore)) {
return host;
}
const gitIgnoreContent = host.read(gitignore).toString();
if (gitIgnoreContent.includes('\n/bazel-out\n')) {
return host;
}
const compiledOutput = '# compiled output\n';
const index = gitIgnoreContent.indexOf(compiledOutput);
const insertionIndex = index >= 0 ? index + compiledOutput.length : gitIgnoreContent.length;
const recorder = host.beginUpdate(gitignore);
recorder.insertRight(insertionIndex, '/bazel-out\n');
host.commitUpdate(recorder);
return host;
};
}
function replacePropertyInAstObject(
recorder: UpdateRecorder, node: JsonAstObject, propertyName: string, value: JsonValue,
indent: number) {
const property = findPropertyInAstObject(node, propertyName);
if (property === null) {
throw new Error(`Property ${propertyName} does not exist in JSON object`);
}
const {start, text} = property;
recorder.remove(start.offset, text.length);
const indentStr = '\n' +
' '.repeat(indent);
const content = JSON.stringify(value, null, ' ').replace(/\n/g, indentStr);
recorder.insertLeft(start.offset, content);
}
function updateAngularJsonToUseBazelBuilder(options: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const {name} = options;
const workspacePath = getWorkspacePath(host);
if (!workspacePath) {
throw new Error('Could not find angular.json');
}
const workspaceContent = host.read(workspacePath).toString();
const workspaceJsonAst = parseJsonAst(workspaceContent) as JsonAstObject;
const projects = findPropertyInAstObject(workspaceJsonAst, 'projects');
if (!projects) {
throw new SchematicsException('Expect projects in angular.json to be an Object');
}
const project = findPropertyInAstObject(projects as JsonAstObject, name);
if (!project) {
throw new SchematicsException(`Expected projects to contain ${name}`);
}
const recorder = host.beginUpdate(workspacePath);
const indent = 8;
const architect =
findPropertyInAstObject(project as JsonAstObject, 'architect') as JsonAstObject;
replacePropertyInAstObject(
recorder, architect, 'build', {
builder: '@angular/bazel:build',
options: {
targetLabel: '//src:bundle.js',
bazelCommand: 'build',
},
configurations: {
production: {
targetLabel: '//src:bundle',
},
},
},
indent);
replacePropertyInAstObject(
recorder, architect, 'serve', {
builder: '@angular/bazel:build',
options: {
targetLabel: '//src:devserver',
bazelCommand: 'run',
},
configurations: {
production: {
targetLabel: '//src:prodserver',
},
},
},
indent);
replacePropertyInAstObject(
recorder, architect, 'test', {
builder: '@angular/bazel:build',
options: {'bazelCommand': 'test', 'targetLabel': '//src/...'},
},
indent);
const e2e = `${options.name}-e2e`;
const e2eNode = findPropertyInAstObject(projects as JsonAstObject, e2e);
if (e2eNode) {
const architect =
findPropertyInAstObject(e2eNode as JsonAstObject, 'architect') as JsonAstObject;
replacePropertyInAstObject(
recorder, architect, 'e2e', {
builder: '@angular/bazel:build',
options: {
bazelCommand: 'test',
targetLabel: '//e2e:devserver_test',
},
configurations: {
production: {
targetLabel: '//e2e:prodserver_test',
},
}
},
indent);
}
host.commitUpdate(recorder);
return host;
};
}
/**
* Create a backup for the original angular.json file in case user wants to
* eject Bazel and revert to the original workflow.
*/
function backupAngularJson(): Rule {
return (host: Tree, context: SchematicContext) => {
const workspacePath = getWorkspacePath(host);
if (!workspacePath) {
return;
}
host.create(
`${workspacePath}.bak`, '// This is a backup file of the original angular.json. ' +
'This file is needed in case you want to revert to the workflow without Bazel.\n\n' +
host.read(workspacePath));
};
}
export default function(options: Schema): Rule {
return (host: Tree) => {
validateProjectName(options.name);
return chain([
schematic('bazel-workspace', options),
addDevAndProdMainForAot(options),
addDevDependenciesToPackageJson(options),
backupAngularJson(),
updateAngularJsonToUseBazelBuilder(options),
updateGitignore(),
]);
};
}

View File

@ -0,0 +1,171 @@
/**
* @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 {HostTree} from '@angular-devkit/schematics';
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
describe('ng-add schematic', () => {
const defaultOptions = {name: 'demo'};
let host: UnitTestTree;
let schematicRunner: SchematicTestRunner;
beforeEach(() => {
host = new UnitTestTree(new HostTree());
host.create('package.json', JSON.stringify({
name: 'demo',
dependencies: {
'@angular/core': '1.2.3',
},
devDependencies: {
'typescript': '3.2.2',
},
}));
host.create('angular.json', JSON.stringify({
projects: {
'demo': {
architect: {
build: {},
serve: {},
test: {},
'extract-i18n': {
builder: '@angular-devkit/build-angular:extract-i18n',
},
},
},
'demo-e2e': {
architect: {
e2e: {},
lint: {
builder: '@angular-devkit/build-angular:tslint',
},
},
},
},
}));
schematicRunner =
new SchematicTestRunner('@angular/bazel', require.resolve('../collection.json'));
});
it('throws if package.json is not found', () => {
expect(host.files).toContain('/package.json');
host.delete('/package.json');
expect(() => schematicRunner.runSchematic('ng-add', defaultOptions))
.toThrowError('Could not find package.json');
});
it('throws if angular.json is not found', () => {
expect(host.files).toContain('/angular.json');
host.delete('/angular.json');
expect(() => schematicRunner.runSchematic('ng-add', defaultOptions, host))
.toThrowError('Could not find angular.json');
});
it('should add @angular/bazel to package.json dependencies', () => {
host = schematicRunner.runSchematic('ng-add', defaultOptions, host);
const {files} = host;
expect(files).toContain('/package.json');
const content = host.readContent('/package.json');
expect(() => JSON.parse(content)).not.toThrow();
const json = JSON.parse(content);
const core = '@angular/core';
const bazel = '@angular/bazel';
expect(Object.keys(json)).toContain('dependencies');
expect(Object.keys(json)).toContain('devDependencies');
expect(Object.keys(json.dependencies)).toContain(core);
expect(Object.keys(json.devDependencies)).toContain(bazel);
expect(json.dependencies[core]).toBe(json.devDependencies[bazel]);
});
it('should add @bazel/* dev dependencies', () => {
host = schematicRunner.runSchematic('ng-add', defaultOptions, host);
const content = host.readContent('/package.json');
const json = JSON.parse(content);
const devDeps = Object.keys(json.devDependencies);
expect(devDeps).toContain('@bazel/bazel');
expect(devDeps).toContain('@bazel/ibazel');
expect(devDeps).toContain('@bazel/karma');
expect(devDeps).toContain('@bazel/typescript');
});
it('should create Bazel workspace file', () => {
host = schematicRunner.runSchematic('ng-add', defaultOptions, host);
const {files} = host;
expect(files).toContain('/WORKSPACE');
expect(files).toContain('/BUILD.bazel');
});
it('should produce main.dev.ts and main.prod.ts for AOT', () => {
host.create('/src/main.ts', 'generated by CLI');
host = schematicRunner.runSchematic('ng-add', defaultOptions, host);
const {files} = host;
// main.dev.ts and main.prod.ts are used by Bazel for AOT
expect(files).toContain('/src/main.dev.ts');
expect(files).toContain('/src/main.prod.ts');
// main.ts is produced by original ng-add schematics
// This file should be present for backwards compatibility.
expect(files).toContain('/src/main.ts');
});
it('should not overwrite index.html with script tags', () => {
host.create('/src/index.html', '<html>Hello World</html>');
host = schematicRunner.runSchematic('ng-add', defaultOptions, host);
const {files} = host;
expect(files).toContain('/src/index.html');
const content = host.readContent('/src/index.html');
expect(content).not.toMatch('<script src="/zone.min.js"></script>');
expect(content).not.toMatch('<script src="/bundle.min.js"></script>');
});
it('should generate main.dev.ts and main.prod.ts', () => {
host = schematicRunner.runSchematic('ng-add', defaultOptions, host);
const {files} = host;
expect(files).toContain('/src/main.dev.ts');
expect(files).toContain('/src/main.prod.ts');
});
it('should overwrite .gitignore for bazel-out directory', () => {
host.create('.gitignore', '\n# compiled output\n');
host = schematicRunner.runSchematic('ng-add', defaultOptions, host);
const {files} = host;
expect(files).toContain('/.gitignore');
const content = host.readContent('/.gitignore');
expect(content).toMatch('\n# compiled output\n/bazel-out\n');
});
it('should create a backup for original angular.json', () => {
expect(host.files).toContain('/angular.json');
const original = host.readContent('/angular.json');
host = schematicRunner.runSchematic('ng-add', defaultOptions, host);
expect(host.files).toContain('/angular.json.bak');
const content = host.readContent('/angular.json.bak');
expect(content.startsWith('// This is a backup file')).toBe(true);
expect(content).toMatch(original);
});
it('should update angular.json to use Bazel builder', () => {
host = schematicRunner.runSchematic('ng-add', defaultOptions, host);
const {files} = host;
expect(files).toContain('/angular.json');
const content = host.readContent('/angular.json');
expect(() => JSON.parse(content)).not.toThrow();
const json = JSON.parse(content);
const demo = json.projects.demo;
const demo_e2e = json.projects['demo-e2e'];
const {build, serve, test} = demo.architect;
expect(build.builder).toBe('@angular/bazel:build');
expect(serve.builder).toBe('@angular/bazel:build');
expect(test.builder).toBe('@angular/bazel:build');
const {e2e, lint} = demo_e2e.architect;
expect(e2e.builder).toBe('@angular/bazel:build');
// it should leave non-Bazel commands unmodified
expect(demo.architect['extract-i18n'].builder)
.toBe('@angular-devkit/build-angular:extract-i18n');
expect(lint.builder).toBe('@angular-devkit/build-angular:tslint');
});
});

View File

@ -0,0 +1,13 @@
// THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE
// THE CORRESPONDING JSON SCHEMA FILE. See README.md.
// tslint:disable:no-global-tslint-disable
// tslint:disable
export interface Schema {
/**
* The name of the project.
*/
name: string;
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsAngularBazelNgAdd",
"title": "Angular Bazel Ng Add Schema",
"type": "object",
"properties": {
"name": {
"description": "The name of the project.",
"type": "string",
"format": "html-selector",
"$default": {
"$source": "argv",
"index": 0
}
}
},
"required": [
"name"
]
}

View File

@ -12,11 +12,9 @@ ts_library(
"schema.json",
],
deps = [
"//packages/bazel/src/schematics/bazel-workspace",
"@ngdeps//@angular-devkit/core",
"//packages/bazel/src/schematics/ng-add",
"@ngdeps//@angular-devkit/schematics",
"@ngdeps//@schematics/angular",
"@ngdeps//typescript",
],
)

198
packages/bazel/src/schematics/ng-new/index.ts Executable file → Normal file
View File

@ -8,211 +8,19 @@
* @fileoverview Schematics for ng-new project that builds with Bazel.
*/
import {SchematicContext, apply, applyTemplates, chain, externalSchematic, MergeStrategy, mergeWith, move, Rule, schematic, Tree, url, SchematicsException, UpdateRecorder,} from '@angular-devkit/schematics';
import {parseJsonAst, JsonAstObject, strings, JsonValue} from '@angular-devkit/core';
import {findPropertyInAstObject, insertPropertyInAstObjectInOrder} from '@schematics/angular/utility/json-utils';
import {Rule, Tree, chain, externalSchematic, schematic} from '@angular-devkit/schematics';
import {validateProjectName} from '@schematics/angular/utility/validation';
import {getWorkspace} from '@schematics/angular/utility/config';
import {Schema} from './schema';
/**
* Packages that build under Bazel require additional dev dependencies. This
* function adds those dependencies to "devDependencies" section in
* package.json.
*/
function addDevDependenciesToPackageJson(options: Schema) {
return (host: Tree) => {
const packageJson = `${options.name}/package.json`;
if (!host.exists(packageJson)) {
throw new Error(`Could not find ${packageJson}`);
}
const packageJsonContent = host.read(packageJson);
if (!packageJsonContent) {
throw new Error('Failed to read package.json content');
}
const jsonAst = parseJsonAst(packageJsonContent.toString()) as JsonAstObject;
const deps = findPropertyInAstObject(jsonAst, 'dependencies') as JsonAstObject;
const devDeps = findPropertyInAstObject(jsonAst, 'devDependencies') as JsonAstObject;
const angularCoreNode = findPropertyInAstObject(deps, '@angular/core');
const angularCoreVersion = angularCoreNode !.value as string;
const devDependencies: {[k: string]: string} = {
'@angular/bazel': angularCoreVersion,
// TODO(kyliau): Consider moving this to latest-versions.ts
'@bazel/bazel': '^0.22.1',
'@bazel/ibazel': '^0.9.0',
'@bazel/karma': '^0.22.1',
'@bazel/typescript': '^0.22.1',
};
const recorder = host.beginUpdate(packageJson);
for (const packageName of Object.keys(devDependencies)) {
const version = devDependencies[packageName];
const indent = 4;
insertPropertyInAstObjectInOrder(recorder, devDeps, packageName, version, indent);
}
host.commitUpdate(recorder);
return host;
};
}
/**
* Append main.dev.ts and main.prod.ts to src directory. These files are needed
* by Bazel for devserver and prodserver, respectively. They are different from
* main.ts generated by CLI because they use platformBrowser (AOT) instead of
* platformBrowserDynamic (JIT).
*/
function addDevAndProdMainForAot(options: Schema) {
return (host: Tree) => {
let newProjectRoot = '';
try {
const workspace = getWorkspace(host);
newProjectRoot = workspace.newProjectRoot || '';
} catch {
}
const srcDir = `${newProjectRoot}/${options.name}/src`;
return mergeWith(apply(url('./files'), [
applyTemplates({
utils: strings,
...options,
'dot': '.',
}),
move(srcDir),
]));
};
}
function overwriteGitignore(options: Schema) {
return (host: Tree) => {
const gitignore = `${options.name}/.gitignore`;
if (!host.exists(gitignore)) {
return host;
}
const gitIgnoreContent = host.read(gitignore);
if (!gitIgnoreContent) {
throw new Error('Failed to read .gitignore content');
}
if (gitIgnoreContent.includes('/bazel-out\n')) {
return host;
}
const lines = gitIgnoreContent.toString().split(/\n/g);
const recorder = host.beginUpdate(gitignore);
const compileOutput = lines.findIndex((line: string) => line === '# compiled output');
recorder.insertRight(compileOutput, '\n/bazel-out');
host.commitUpdate(recorder);
return host;
};
}
function replacePropertyInAstObject(
recorder: UpdateRecorder, node: JsonAstObject, propertyName: string, value: JsonValue,
indent: number) {
const property = findPropertyInAstObject(node, propertyName);
if (property === null) {
throw new Error(`Property ${propertyName} does not exist in JSON object`);
}
const {start, text} = property;
recorder.remove(start.offset, text.length);
const indentStr = '\n' +
' '.repeat(indent);
const content = JSON.stringify(value, null, ' ').replace(/\n/g, indentStr);
recorder.insertLeft(start.offset, content);
}
function updateWorkspaceFileToUseBazelBuilder(options: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const {name} = options;
const workspacePath = `${name}/angular.json`;
if (!host.exists(workspacePath)) {
throw new SchematicsException(`Workspace file ${workspacePath} not found.`);
}
const workspaceBuffer = host.read(workspacePath) !;
const workspaceJsonAst = parseJsonAst(workspaceBuffer.toString()) as JsonAstObject;
const projects = findPropertyInAstObject(workspaceJsonAst, 'projects');
if (!projects) {
throw new SchematicsException('Expect projects in angular.json to be an Object');
}
const project = findPropertyInAstObject(projects as JsonAstObject, name);
if (!project) {
throw new SchematicsException(`Expected projects to contain ${name}`);
}
const recorder = host.beginUpdate(workspacePath);
const indent = 6;
replacePropertyInAstObject(
recorder, project as JsonAstObject, 'architect', {
'build': {
'builder': '@angular/bazel:build',
'options': {'targetLabel': '//src:bundle.js', 'bazelCommand': 'build'},
'configurations': {'production': {'targetLabel': '//src:bundle'}}
},
'serve': {
'builder': '@angular/bazel:build',
'options': {'targetLabel': '//src:devserver', 'bazelCommand': 'run'},
'configurations': {'production': {'targetLabel': '//src:prodserver'}}
},
'extract-i18n': {
'builder': '@angular-devkit/build-angular:extract-i18n',
'options': {'browserTarget': `${name}:build`}
},
'test': {
'builder': '@angular/bazel:build',
'options': {'bazelCommand': 'test', 'targetLabel': '//src/...'}
},
'lint': {
'builder': '@angular-devkit/build-angular:tslint',
'options': {
'tsConfig': ['src/tsconfig.app.json', 'src/tsconfig.spec.json'],
'exclude': ['**/node_modules/**']
}
}
},
indent);
const e2e = `${options.name}-e2e`;
const e2eNode = findPropertyInAstObject(projects as JsonAstObject, e2e);
if (e2eNode) {
replacePropertyInAstObject(
recorder, e2eNode as JsonAstObject, 'architect', {
'e2e': {
'builder': '@angular/bazel:build',
'options': {'bazelCommand': 'test', 'targetLabel': '//e2e:devserver_test'},
'configurations': {'production': {'targetLabel': '//e2e:prodserver_test'}}
},
'lint': {
'builder': '@angular-devkit/build-angular:tslint',
'options': {'tsConfig': 'e2e/tsconfig.e2e.json', 'exclude': ['**/node_modules/**']}
}
},
indent);
}
host.commitUpdate(recorder);
return host;
};
}
export default function(options: Schema): Rule {
return (host: Tree) => {
validateProjectName(options.name);
return chain([
externalSchematic(
'@schematics/angular', 'ng-new',
{
...options,
}),
addDevDependenciesToPackageJson(options),
addDevAndProdMainForAot(options),
schematic('bazel-workspace', options, {
externalSchematic('@schematics/angular', 'ng-new', options),
schematic('ng-add', options, {
scope: options.name,
}),
overwriteGitignore(options),
updateWorkspaceFileToUseBazelBuilder(options),
]);
};
}

View File

@ -8,7 +8,7 @@
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
describe('Ng-new Schematic', () => {
describe('ng-new schematic', () => {
const schematicRunner =
new SchematicTestRunner('@angular/bazel', require.resolve('../collection.json'), );
const defaultOptions = {
@ -22,94 +22,15 @@ describe('Ng-new Schematic', () => {
const {files} = host;
// External schematic should produce workspace file angular.json
expect(files).toContain('/demo/angular.json');
});
it('should add @angular/bazel to package.json dependencies', () => {
const options = {...defaultOptions};
const host = schematicRunner.runSchematic('ng-new', options);
const {files} = host;
expect(files).toContain('/demo/package.json');
const content = host.readContent('/demo/package.json');
expect(() => JSON.parse(content)).not.toThrow();
const json = JSON.parse(content);
const core = '@angular/core';
const bazel = '@angular/bazel';
expect(Object.keys(json)).toContain('dependencies');
expect(Object.keys(json)).toContain('devDependencies');
expect(Object.keys(json.dependencies)).toContain(core);
expect(Object.keys(json.devDependencies)).toContain(bazel);
expect(json.dependencies[core]).toBe(json.devDependencies[bazel]);
});
it('should add @bazel/* dev dependencies', () => {
const options = {...defaultOptions};
const host = schematicRunner.runSchematic('ng-new', options);
const content = host.readContent('/demo/package.json');
const json = JSON.parse(content);
const devDeps = Object.keys(json.devDependencies);
expect(devDeps).toContain('@bazel/karma');
expect(devDeps).toContain('@bazel/typescript');
});
it('should create Bazel workspace file', () => {
it('should call ng-add to generate Bazel files', () => {
const options = {...defaultOptions};
const host = schematicRunner.runSchematic('ng-new', options);
const {files} = host;
expect(files).toContain('/demo/WORKSPACE');
expect(files).toContain('/demo/BUILD.bazel');
});
it('should produce main.prod.ts for AOT', () => {
const options = {...defaultOptions};
const host = schematicRunner.runSchematic('ng-new', options);
const {files} = host;
// main.prod.ts is used by Bazel for AOT
expect(files).toContain('/demo/src/main.prod.ts');
// main.ts is produced by original ng-new schematics
// This file should be present for backwards compatibility.
expect(files).toContain('/demo/src/main.ts');
});
it('should not overwrite index.html with script tags', () => {
const options = {...defaultOptions};
const host = schematicRunner.runSchematic('ng-new', options);
const {files} = host;
expect(files).toContain('/demo/src/index.html');
const content = host.readContent('/demo/src/index.html');
expect(content).not.toMatch('<script src="/zone.min.js"></script>');
expect(content).not.toMatch('<script src="/bundle.min.js"></script>');
});
it('should generate main.dev.ts and main.prod.ts', () => {
const options = {...defaultOptions};
const host = schematicRunner.runSchematic('ng-new', options);
const {files} = host;
expect(files).toContain('/demo/src/main.dev.ts');
expect(files).toContain('/demo/src/main.prod.ts');
});
it('should overwrite .gitignore for bazel-out directory', () => {
const options = {...defaultOptions};
const host = schematicRunner.runSchematic('ng-new', options);
const {files} = host;
expect(files).toContain('/demo/.gitignore');
const content = host.readContent('/demo/.gitignore');
expect(content).toMatch('/bazel-out');
});
it('should update angular.json to use Bazel builder', () => {
const options = {...defaultOptions};
const host = schematicRunner.runSchematic('ng-new', options);
const {files} = host;
expect(files).toContain('/demo/angular.json');
const content = host.readContent('/demo/angular.json');
expect(() => JSON.parse(content)).not.toThrow();
const json = JSON.parse(content);
let {architect} = json.projects.demo;
expect(architect.build.builder).toBe('@angular/bazel:build');
expect(architect.serve.builder).toBe('@angular/bazel:build');
expect(architect.test.builder).toBe('@angular/bazel:build');
architect = json.projects['demo-e2e'].architect;
expect(architect.e2e.builder).toBe('@angular/bazel:build');
expect(files).toContain('/demo/src/BUILD.bazel');
});
});

32
packages/bazel/src/schematics/ng-new/schema.json Executable file → Normal file
View File

@ -43,7 +43,9 @@
"commit": {
"description": "Initial repository commit information.",
"oneOf": [
{ "type": "boolean" },
{
"type": "boolean"
},
{
"type": "object",
"properties": {
@ -85,7 +87,12 @@
},
"viewEncapsulation": {
"description": "Specifies the view encapsulation strategy.",
"enum": ["Emulated", "Native", "None", "ShadowDom"],
"enum": [
"Emulated",
"Native",
"None",
"ShadowDom"
],
"type": "string"
},
"version": {
@ -118,11 +125,22 @@
"message": "Which stylesheet format would you like to use?",
"type": "list",
"items": [
{ "value": "css", "label": "CSS" },
{ "value": "scss", "label": "SCSS [ http://sass-lang.com ]" },
{ "value": "sass", "label": "SASS [ http://sass-lang.com ]" },
{ "value": "less", "label": "LESS [ http://lesscss.org ]" },
{ "value": "styl", "label": "Stylus [ http://stylus-lang.com ]" }
{
"value": "css",
"label": "CSS"
},
{
"value": "sass",
"label": "Sass [ http://sass-lang.com ]"
},
{
"value": "less",
"label": "Less [ http://lesscss.org ]"
},
{
"value": "styl",
"label": "Stylus [ http://stylus-lang.com ]"
}
]
}
},