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:

committed by
Matias Niemelä

parent
744b0205e2
commit
4a92fa9471
@ -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",
|
||||
],
|
||||
)
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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',
|
||||
|
@ -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', () => {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
36
packages/bazel/src/schematics/ng-add/BUILD.bazel
Normal file
36
packages/bazel/src/schematics/ng-add/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
232
packages/bazel/src/schematics/ng-add/index.ts
Executable file
232
packages/bazel/src/schematics/ng-add/index.ts
Executable 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(),
|
||||
]);
|
||||
};
|
||||
}
|
171
packages/bazel/src/schematics/ng-add/index_spec.ts
Normal file
171
packages/bazel/src/schematics/ng-add/index_spec.ts
Normal 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');
|
||||
});
|
||||
});
|
13
packages/bazel/src/schematics/ng-add/schema.d.ts
vendored
Normal file
13
packages/bazel/src/schematics/ng-add/schema.d.ts
vendored
Normal 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;
|
||||
}
|
20
packages/bazel/src/schematics/ng-add/schema.json
Executable file
20
packages/bazel/src/schematics/ng-add/schema.json
Executable 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"
|
||||
]
|
||||
}
|
@ -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
198
packages/bazel/src/schematics/ng-new/index.ts
Executable file → Normal 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),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
@ -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
32
packages/bazel/src/schematics/ng-new/schema.json
Executable file → Normal 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 ]"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user