refactor(bazel): remove old Angular CLI schematics and builder (#41575)

This is leftover code which is no longer used.

PR Close #41575
This commit is contained in:
Alan Agius 2021-04-12 11:34:24 +02:00 committed by Zach Arend
parent c7f9516ab9
commit 71b8c9ab29
17 changed files with 1 additions and 1451 deletions

View File

@ -42,7 +42,6 @@
"// 1": "dependencies are used locally and by bazel",
"dependencies": {
"@angular/cli": "12.0.0-next.7",
"@angular-devkit/architect": "0.1200.0-next.7",
"@angular-devkit/build-angular": "github:angular/angular-devkit-build-angular-builds#e0c1374b12cfc892844030431bc19f631c0086a5",
"@angular-devkit/build-optimizer": "0.1200.0-next.7",
"@angular-devkit/core": "12.0.0-next.7",

View File

@ -13,9 +13,6 @@ pkg_npm(
"//packages/bazel/src/ngc-wrapped:package_assets",
"//packages/bazel/third_party/github.com/bazelbuild/bazel/src/main/protobuf:package_assets",
],
nested_packages = [
"//packages/bazel/docs",
],
substitutions = {
"(#|//)\\s+BEGIN-DEV-ONLY[\\w\\W]+?(#|//)\\s+END-DEV-ONLY": "",
"//packages/bazel/": "//@angular/bazel/",

View File

@ -45,7 +45,7 @@ This new rule leverages ngtsc plugin supported by `ts_library`, and it is much f
For the latest recommendations, please refer to the canonical Angular Bazel [repo](https://github.com/bazelbuild/rules_nodejs/tree/master/examples/angular).
For questions, please ask in the `#angular` channel in https://slack.bazel.build/.
For questions, please ask in the `#angular` channel in http://slack.bazel.build/.
## Angular CLI

View File

@ -1,13 +0,0 @@
load("@io_bazel_skydoc//skylark:skylark.bzl", "skylark_doc")
skylark_doc(
name = "docs",
srcs = [
"//packages/bazel/src:ng_module.bzl",
"//packages/bazel/src/ng_package:ng_package.bzl",
],
format = "html",
site_root = "/bazel-builds",
strip_prefix = "packages/bazel/",
visibility = ["//packages/bazel:__subpackages__"],
)

View File

@ -1,200 +0,0 @@
/**
* @license
* Copyright Google LLC 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
*/
/// <reference types='node'/>
import {spawn} from 'child_process';
import {copyFileSync, existsSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync} from 'fs';
import {platform} from 'os';
import {dirname, join, normalize} from 'path';
export type Executable = 'bazel'|'ibazel';
export type Command = 'build'|'test'|'run'|'coverage'|'query';
/**
* Spawn the Bazel process. Trap SINGINT to make sure Bazel process is killed.
*/
export function runBazel(
projectDir: string, binary: string, command: Command, workspaceTarget: string,
flags: string[]) {
projectDir = normalize(projectDir);
binary = normalize(binary);
return new Promise((resolve, reject) => {
const buildProcess = spawn(binary, [command, workspaceTarget, ...flags], {
cwd: projectDir,
stdio: 'inherit',
});
process.on('SIGINT', (signal) => {
if (!buildProcess.killed) {
buildProcess.kill();
reject(new Error(`Bazel process received ${signal}.`));
}
});
buildProcess.once('close', (code: number) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`${binary} failed with code ${code}.`));
}
});
});
}
/**
* Resolves the path to `@bazel/bazel` or `@bazel/ibazel`.
*/
export function checkInstallation(name: Executable, projectDir: string): string {
projectDir = normalize(projectDir);
const packageName = `@bazel/${name}`;
try {
const bazelPath = require.resolve(packageName, {
paths: [projectDir],
});
return require(bazelPath).getNativeBinary();
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
throw new Error(
`Could not run ${name}. Please make sure that the ` +
`"${name}" command is installed by running ` +
`"npm install ${packageName}" or "yarn install ${packageName}".`);
}
throw error;
}
}
/**
* Returns the absolute path to the template directory in `@angular/bazel`.
*/
export function getTemplateDir(root: string): string {
root = normalize(root);
const packageJson = require.resolve('@angular/bazel/package.json', {
paths: [root],
});
const packageDir = dirname(packageJson);
const templateDir = join(packageDir, 'src', 'builders', 'files');
if (!statSync(templateDir).isDirectory()) {
throw new Error('Could not find Bazel template directory in "@angular/bazel".');
}
return templateDir;
}
/**
* Recursively list the specified 'dir' using depth-first approach. Paths
* returned are relative to 'dir'.
*/
function listR(dir: string): string[] {
function list(dir: string, root: string, results: string[]) {
const paths = readdirSync(dir);
for (const path of paths) {
const absPath = join(dir, path);
const relPath = join(root, path);
if (statSync(absPath).isFile()) {
results.push(relPath);
} else {
list(absPath, relPath, results);
}
}
return results;
}
return list(dir, '', []);
}
/**
* Return the name of the lock file that is present in the specified 'root'
* directory. If none exists, default to creating an empty yarn.lock file.
*/
function getOrCreateLockFile(root: string): 'yarn.lock'|'package-lock.json' {
const yarnLock = join(root, 'yarn.lock');
if (existsSync(yarnLock)) {
return 'yarn.lock';
}
const npmLock = join(root, 'package-lock.json');
if (existsSync(npmLock)) {
return 'package-lock.json';
}
// Prefer yarn if no lock file exists
writeFileSync(yarnLock, '');
return 'yarn.lock';
}
// Replace yarn_install rule with npm_install and copy from 'source' to 'dest'.
function replaceYarnWithNpm(source: string, dest: string) {
const srcContent = readFileSync(source, 'utf-8');
const destContent = srcContent.replace(/yarn_install/g, 'npm_install')
.replace('yarn_lock', 'package_lock_json')
.replace('yarn.lock', 'package-lock.json');
writeFileSync(dest, destContent);
}
/**
* Disable sandbox on Mac OS by setting spawn_strategy in .bazelrc.
* For a hello world (ng new) application, removing the sandbox improves build
* time by almost 40%.
* ng build with sandbox: 22.0 seconds
* ng build without sandbox: 13.3 seconds
*/
function disableSandbox(source: string, dest: string) {
const srcContent = readFileSync(source, 'utf-8');
const destContent = `${srcContent}
# Disable sandbox on Mac OS for performance reason.
build --spawn_strategy=local
run --spawn_strategy=local
test --spawn_strategy=local
`;
writeFileSync(dest, destContent);
}
/**
* Copy Bazel files (WORKSPACE, BUILD.bazel, etc) from the template directory to
* the project `root` directory, and return the absolute paths of the files
* copied, so that they can be deleted later.
* Existing files in `root` will not be replaced.
*/
export function copyBazelFiles(root: string, templateDir: string) {
root = normalize(root);
templateDir = normalize(templateDir);
const bazelFiles: string[] = [];
const templates = listR(templateDir);
const useYarn = getOrCreateLockFile(root) === 'yarn.lock';
for (const template of templates) {
const name = template.replace('__dot__', '.').replace('.template', '');
const source = join(templateDir, template);
const dest = join(root, name);
try {
if (!existsSync(dest)) {
if (!useYarn && name === 'WORKSPACE') {
replaceYarnWithNpm(source, dest);
} else if (platform() === 'darwin' && name === '.bazelrc') {
disableSandbox(source, dest);
} else {
copyFileSync(source, dest);
}
bazelFiles.push(dest);
}
} catch {
}
}
return bazelFiles;
}
/**
* Delete the specified 'files'. This function never throws.
*/
export function deleteBazelFiles(files: string[]) {
for (const file of files) {
try {
unlinkSync(file);
} catch {
}
}
}

View File

@ -1,41 +0,0 @@
/**
* @license
* Copyright Google LLC 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 Bazel builder
*/
import {BuilderContext, BuilderOutput, createBuilder,} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
import {checkInstallation, copyBazelFiles, deleteBazelFiles, getTemplateDir, runBazel} from './bazel';
import {Schema} from './schema';
async function _bazelBuilder(
options: JsonObject&Schema,
context: BuilderContext,
): Promise<BuilderOutput> {
const {logger, workspaceRoot} = context;
const {bazelCommand, leaveBazelFilesOnDisk, targetLabel, watch} = options;
const executable = watch ? 'ibazel' : 'bazel';
const binary = checkInstallation(executable, workspaceRoot);
const templateDir = getTemplateDir(workspaceRoot);
const bazelFiles = copyBazelFiles(workspaceRoot, templateDir);
try {
const flags: string[] = [];
await runBazel(workspaceRoot, binary, bazelCommand, targetLabel, flags);
return {success: true};
} catch (err) {
logger.error(err.message);
return {success: false};
} finally {
if (!leaveBazelFilesOnDisk) {
deleteBazelFiles(bazelFiles); // this will never throw
}
}
}
export default createBuilder(_bazelBuilder);

View File

@ -1,38 +0,0 @@
/**
* @license
* Copyright Google LLC 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
*/
/**
* Options for Bazel Builder
*/
export interface Schema {
/**
* Common commands supported by Bazel.
*/
bazelCommand: BazelCommand;
/**
* If true, leave Bazel files on disk after running command.
*/
leaveBazelFilesOnDisk?: boolean;
/**
* Target to be executed under Bazel.
*/
targetLabel: string;
/**
* If true, watch the filesystem using ibazel.
*/
watch?: boolean;
}
/**
* Common commands supported by Bazel.
*/
export enum BazelCommand {
Build = 'build',
Run = 'run',
Test = 'test',
}

View File

@ -1,357 +0,0 @@
/**
* @license
* Copyright Google LLC 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 {JsonAstObject, parseJsonAst} from '@angular-devkit/core';
import {apply, applyTemplates, chain, mergeWith, Rule, SchematicContext, SchematicsException, Tree, url} from '@angular-devkit/schematics';
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
import {getWorkspace, getWorkspacePath} from '@schematics/angular/utility/config';
import {addPackageJsonDependency, getPackageJsonDependency, NodeDependencyType, removePackageJsonDependency} from '@schematics/angular/utility/dependencies';
import {findPropertyInAstObject, insertPropertyInAstObjectInOrder} from '@schematics/angular/utility/json-utils';
import {validateProjectName} from '@schematics/angular/utility/validation';
import {isJsonAstObject, replacePropertyInAstObject} from '../utility/json-utils';
import {findE2eArchitect} from '../utility/workspace-utils';
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 angularCore = getPackageJsonDependency(host, '@angular/core');
if (!angularCore) {
throw new Error('@angular/core dependency not found in package.json');
}
// TODO: use a Record<string, string> when the tsc lib setting allows us
const devDependencies: [string, string][] = [
['@angular/bazel', angularCore.version],
['@bazel/bazel', '2.1.0'],
['@bazel/ibazel', '0.12.3'],
['@bazel/karma', '1.6.0'],
['@bazel/protractor', '1.6.0'],
['@bazel/rollup', '1.6.0'],
['@bazel/terser', '1.6.0'],
['@bazel/typescript', '1.6.0'],
['history-server', '1.3.1'],
['html-insert-assets', '0.5.0'],
['karma', '4.4.1'],
['karma-chrome-launcher', '3.1.0'],
['karma-firefox-launcher', '1.2.0'],
['karma-jasmine', '2.0.1'],
['karma-requirejs', '1.1.0'],
['karma-sourcemap-loader', '0.3.7'],
['protractor', '5.4.2'],
['requirejs', '2.3.6'],
['rollup', '1.27.5'],
['rollup-plugin-commonjs', '10.1.0'],
['rollup-plugin-node-resolve', '5.2.0'],
['terser', '4.4.0'],
];
for (const [name, version] of devDependencies) {
const dep = getPackageJsonDependency(host, name);
if (dep && dep.type !== NodeDependencyType.Dev) {
removePackageJsonDependency(host, name);
}
addPackageJsonDependency(host, {
name,
version,
type: NodeDependencyType.Dev,
overwrite: true,
});
}
};
}
/**
* Remove packages that are not needed under Bazel.
* @param options
*/
function removeObsoleteDependenciesFromPackageJson(options: Schema) {
return (host: Tree) => {
const depsToRemove = [
'@angular-devkit/build-angular',
];
for (const packageName of depsToRemove) {
removePackageJsonDependency(host, packageName);
}
};
}
/**
* Append additional Javascript / Typescript files needed to compile an Angular
* project under Bazel.
*/
function addFilesRequiredByBazel(options: Schema) {
return (host: Tree) => {
return mergeWith(apply(url('./files'), [
applyTemplates({}),
]));
};
}
/**
* Append '/bazel-out' to the gitignore file.
*/
function updateGitignore() {
return (host: Tree) => {
const gitignore = '/.gitignore';
if (!host.exists(gitignore)) {
return host;
}
const gitIgnoreContentRaw = host.read(gitignore);
if (!gitIgnoreContentRaw) {
return host;
}
const gitIgnoreContent = gitIgnoreContentRaw.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;
};
}
/**
* Change the architect in angular.json to use Bazel builder.
*/
function updateAngularJsonToUseBazelBuilder(options: Schema): Rule {
return (host: Tree) => {
const name = options.name!;
const workspacePath = getWorkspacePath(host);
if (!workspacePath) {
throw new Error('Could not find angular.json');
}
const workspaceContent = host.read(workspacePath);
if (!workspaceContent) {
throw new Error('Failed to read angular.json content');
}
const workspaceJsonAst = parseJsonAst(workspaceContent.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 = 8;
const architect =
findPropertyInAstObject(project as JsonAstObject, 'architect') as JsonAstObject;
replacePropertyInAstObject(
recorder, architect, 'build', {
builder: '@angular/bazel:build',
options: {
targetLabel: '//src:prodapp',
bazelCommand: 'build',
},
configurations: {
production: {
targetLabel: '//src:prodapp',
},
},
},
indent);
replacePropertyInAstObject(
recorder, architect, 'serve', {
builder: '@angular/bazel:build',
options: {
targetLabel: '//src:devserver',
bazelCommand: 'run',
watch: true,
},
configurations: {
production: {
targetLabel: '//src:prodserver',
},
},
},
indent);
if (findPropertyInAstObject(architect, 'test')) {
replacePropertyInAstObject(
recorder, architect, 'test', {
builder: '@angular/bazel:build',
options: {
bazelCommand: 'test',
targetLabel: '//src:test',
},
},
indent);
}
const e2eArchitect = findE2eArchitect(workspaceJsonAst, name);
if (e2eArchitect && findPropertyInAstObject(e2eArchitect, 'e2e')) {
replacePropertyInAstObject(
recorder, e2eArchitect, '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));
};
}
/**
* @angular/bazel requires minimum version of rxjs to be 6.4.0. This function
* upgrades the version of rxjs in package.json if necessary.
*/
function upgradeRxjs() {
return (host: Tree, context: SchematicContext) => {
const rxjsNode = getPackageJsonDependency(host, 'rxjs');
if (!rxjsNode) {
throw new Error(`Failed to find rxjs dependency.`);
}
const match = rxjsNode.version.match(/(\d)+\.(\d)+.(\d)+$/);
if (match) {
const [_, major, minor] = match;
if (major < '6' || (major === '6' && minor < '5')) {
addPackageJsonDependency(host, {
...rxjsNode,
version: '~6.5.3',
overwrite: true,
});
}
} else {
context.logger.info(
'Could not determine version of rxjs. \n' +
'Please make sure that version is at least 6.5.3.');
}
return host;
};
}
/**
* When using Ivy, ngcc must be run as a postinstall step.
* This function adds this postinstall step.
*/
function addPostinstallToRunNgcc() {
return (host: Tree, context: SchematicContext) => {
const packageJson = 'package.json';
if (!host.exists(packageJson)) {
throw new Error(`Could not find ${packageJson}`);
}
const content = host.read(packageJson);
if (!content) {
throw new Error('Failed to read package.json content');
}
const jsonAst = parseJsonAst(content.toString());
if (!isJsonAstObject(jsonAst)) {
throw new Error(`Failed to parse JSON for ${packageJson}`);
}
const scripts = findPropertyInAstObject(jsonAst, 'scripts') as JsonAstObject;
const recorder = host.beginUpdate(packageJson);
// For bazel we need to compile the all files in place so we
// don't use `--first-only` or `--create-ivy-entry-points`
const ngccCommand = 'ngcc --properties es2015 browser module main';
if (scripts) {
const postInstall = findPropertyInAstObject(scripts, 'postinstall');
if (postInstall && postInstall.value) {
let value = postInstall.value as string;
if (/\bngcc\b/.test(value)) {
// `ngcc` is already in the postinstall script
value =
value.replace(/\s*--first-only\b/, '').replace(/\s*--create-ivy-entry-points\b/, '');
replacePropertyInAstObject(recorder, scripts, 'postinstall', value);
} else {
const command = `${postInstall.value}; ${ngccCommand}`;
replacePropertyInAstObject(recorder, scripts, 'postinstall', command);
}
} else {
insertPropertyInAstObjectInOrder(recorder, scripts, 'postinstall', ngccCommand, 4);
}
} else {
insertPropertyInAstObjectInOrder(
recorder, jsonAst, 'scripts', {
postinstall: ngccCommand,
},
2);
}
host.commitUpdate(recorder);
return host;
};
}
/**
* Schedule a task to perform npm / yarn install.
*/
function installNodeModules(options: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
if (!options.skipInstall) {
context.addTask(new NodePackageInstallTask());
}
};
}
export default function(options: Schema): Rule {
return (host: Tree) => {
options.name = options.name || getWorkspace(host).defaultProject;
if (!options.name) {
throw new Error('Please specify a project using "--name project-name"');
}
validateProjectName(options.name);
return chain([
addFilesRequiredByBazel(options),
addDevDependenciesToPackageJson(options),
removeObsoleteDependenciesFromPackageJson(options),
addPostinstallToRunNgcc(),
backupAngularJson(),
updateAngularJsonToUseBazelBuilder(options),
updateGitignore(),
upgradeRxjs(),
installNodeModules(options),
]);
};
}

View File

@ -1,340 +0,0 @@
/**
* @license
* Copyright Google LLC 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',
'rxjs': '~6.3.3',
},
devDependencies: {
'typescript': '3.2.2',
},
}));
host.create('tsconfig.json', JSON.stringify({
compileOnSave: false,
compilerOptions: {
baseUrl: './',
outDir: './dist/out-tsc',
}
}));
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',
},
},
},
},
defaultProject: 'demo',
}));
schematicRunner =
new SchematicTestRunner('@angular/bazel', require.resolve('../collection.json'));
});
it('throws if package.json is not found', async () => {
expect(host.files).toContain('/package.json');
host.delete('/package.json');
let message = 'No error';
try {
await schematicRunner.runSchematicAsync('ng-add', defaultOptions).toPromise();
} catch (e) {
message = e.message;
}
expect(message).toBe('Could not read package.json.');
});
it('throws if angular.json is not found', async () => {
expect(host.files).toContain('/angular.json');
host.delete('/angular.json');
let message = 'No error';
try {
await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
} catch (e) {
message = e.message;
}
expect(message).toBe('Could not find angular.json');
});
it('should add @angular/bazel to package.json dependencies', async () => {
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
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);
});
it('should add @bazel/* dev dependencies', async () => {
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
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/protractor');
expect(devDeps).toContain('@bazel/typescript');
});
it('should replace an existing dev dependency', async () => {
expect(host.files).toContain('/package.json');
const packageJson = JSON.parse(host.readContent('/package.json'));
packageJson.devDependencies['@angular/bazel'] = '4.2.42';
host.overwrite('/package.json', JSON.stringify(packageJson));
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
const content = host.readContent('/package.json');
// It is possible that a dep gets added twice if the package already exists.
expect(content.match(/@angular\/bazel/g)!.length).toEqual(1);
const json = JSON.parse(content);
expect(json.devDependencies['@angular/bazel']).toBe('1.2.3');
});
it('should remove an existing dependency', async () => {
expect(host.files).toContain('/package.json');
const packageJson = JSON.parse(host.readContent('/package.json'));
packageJson.dependencies['@angular/bazel'] = '4.2.42';
expect(Object.keys(packageJson.dependencies)).toContain('@angular/bazel');
host.overwrite('/package.json', JSON.stringify(packageJson));
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
const content = host.readContent('/package.json');
const json = JSON.parse(content);
expect(Object.keys(json.dependencies)).not.toContain('@angular/bazel');
expect(json.devDependencies['@angular/bazel']).toBe('1.2.3');
});
it('should remove unneeded dependencies', async () => {
const packageJson = JSON.parse(host.readContent('/package.json'));
packageJson.devDependencies['@angular-devkit/build-angular'] = '1.2.3';
host.overwrite('/package.json', JSON.stringify(packageJson));
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
const content = host.readContent('/package.json');
const json = JSON.parse(content);
expect(json.devDependencies['angular-devkit/build-angular']).toBeUndefined();
});
it('should append to scripts.postinstall if it already exists', async () => {
const packageJson = JSON.parse(host.readContent('/package.json'));
packageJson['scripts'] = {
postinstall: 'angular rocks',
};
host.overwrite('/package.json', JSON.stringify(packageJson));
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
const content = host.readContent('/package.json');
const json = JSON.parse(content);
expect(json.scripts['postinstall'])
.toBe('angular rocks; ngcc --properties es2015 browser module main');
});
it('should update ngcc in scripts.postinstall if it already exists', async () => {
const packageJson = JSON.parse(host.readContent('/package.json'));
packageJson['scripts'] = {
postinstall:
'ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points',
};
host.overwrite('/package.json', JSON.stringify(packageJson));
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
const content = host.readContent('/package.json');
const json = JSON.parse(content);
expect(json.scripts['postinstall']).toBe('ngcc --properties es2015 browser module main');
});
it('should not create Bazel workspace file', async () => {
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
const {files} = host;
expect(files).not.toContain('/WORKSPACE');
expect(files).not.toContain('/BUILD.bazel');
});
it('should produce main.dev.ts and main.prod.ts for AOT', async () => {
host.create('/src/main.ts', 'generated by CLI');
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
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', async () => {
host.create('/src/index.html', '<html>Hello World</html>');
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
const {files} = host;
expect(files).toContain('/src/index.html');
const content = host.readContent('/src/index.html');
expect(content).not.toMatch('<script src="/zone.umd.min.js"></script>');
expect(content).not.toMatch('<script src="/bundle.min.js"></script>');
});
it('should generate main.dev.ts and main.prod.ts', async () => {
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
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', async () => {
host.create('.gitignore', '\n# compiled output\n');
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
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', async () => {
expect(host.files).toContain('/angular.json');
const original = host.readContent('/angular.json');
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
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', async () => {
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
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');
});
it('should get defaultProject if name is not provided', async () => {
const options = {};
host = await schematicRunner.runSchematicAsync('ng-add', options, host).toPromise();
const content = host.readContent('/angular.json');
const json = JSON.parse(content);
const builder = json.projects.demo.architect.build.builder;
expect(builder).toBe('@angular/bazel:build');
});
describe('rxjs', () => {
const cases = [
// version|upgrade
['6.3.3', true],
['~6.3.3', true],
['^6.3.3', true],
['~6.3.11', true],
['6.4.0', true],
['~6.4.0', true],
['~6.4.1', true],
['6.5.0', false],
['~6.5.0', false],
['^6.5.0', false],
['~7.0.1', false],
];
for (const [version, upgrade] of cases) {
it(`should ${upgrade ? '' : 'not '}upgrade v${version}')`, async () => {
host.overwrite('package.json', JSON.stringify({
name: 'demo',
dependencies: {
'@angular/core': '1.2.3',
'rxjs': version,
},
devDependencies: {
'typescript': '3.2.2',
},
}));
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
expect(host.files).toContain('/package.json');
const content = host.readContent('/package.json');
const json = JSON.parse(content);
if (upgrade) {
expect(json.dependencies.rxjs).toBe('~6.5.3');
} else {
expect(json.dependencies.rxjs).toBe(version);
}
});
}
});
it('should add a postinstall step to package.json', async () => {
host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
expect(host.files).toContain('/package.json');
const content = host.readContent('/package.json');
const json = JSON.parse(content);
expect(json.scripts.postinstall).toBe('ngcc --properties es2015 browser module main');
});
it('should work when run on a minimal project (without test and e2e targets)', async () => {
host.overwrite('angular.json', JSON.stringify({
projects: {
'demo': {
architect: {
build: {},
serve: {},
'extract-i18n': {
builder: '@angular-devkit/build-angular:extract-i18n',
},
},
},
},
}));
let error: Error|null = null;
try {
await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise();
} catch (e) {
error = e;
}
expect(error).toBeNull();
});
});

View File

@ -1,18 +0,0 @@
/**
* @license
* Copyright Google LLC 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
*/
export interface Schema {
/**
* The name of the project.
*/
name?: string;
/**
* When true, does not install dependency packages.
*/
skipInstall?: boolean;
}

View File

@ -1,33 +0,0 @@
/**
* @license
* Copyright Google LLC 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 {chain, externalSchematic, Rule, schematic, Tree} from '@angular-devkit/schematics';
import {validateProjectName} from '@schematics/angular/utility/validation';
import {Schema} from './schema';
export default function(options: Schema): Rule {
return (host: Tree) => {
validateProjectName(options.name);
return chain([
externalSchematic('@schematics/angular', 'ng-new', options),
schematic(
'ng-add', {
name: options.name,
// skip install since `ng-new` above will schedule the task
skipInstall: true,
},
{
scope: options.name,
}),
]);
};
}

View File

@ -1,37 +0,0 @@
/**
* @license
* Copyright Google LLC 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} from '@angular-devkit/schematics/testing';
describe('ng-new schematic', () => {
const schematicRunner = new SchematicTestRunner(
'@angular/bazel',
require.resolve('../collection.json'),
);
const defaultOptions = {
name: 'demo',
version: '7.0.0',
};
it('should call external @schematics/angular', async () => {
const options = {...defaultOptions};
const host = await schematicRunner.runSchematicAsync('ng-new', options).toPromise();
const {files} = host;
// External schematic should produce workspace file angular.json
expect(files).toContain('/demo/angular.json');
expect(files).toContain('/demo/package.json');
});
it('should call ng-add to generate additional files needed by Bazel', async () => {
const options = {...defaultOptions};
const host = await schematicRunner.runSchematicAsync('ng-new', options).toPromise();
const {files} = host;
expect(files).toContain('/demo/src/main.dev.ts');
expect(files).toContain('/demo/src/main.prod.ts');
});
});

View File

@ -1,112 +0,0 @@
/**
* @license
* Copyright Google LLC 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
*/
export interface Schema {
/**
* Initial git repository commit information.
*/
commit?: CommitUnion;
/**
* When true (the default), creates a new initial app project in the src folder of the new
* workspace. When false, creates an empty workspace with no initial app. You can then use
* the generate application command so that all apps are created in the projects folder.
*/
createApplication?: boolean;
/**
* The directory name to create the workspace in.
*/
directory?: string;
/**
* When true, creates a new app that uses the Ivy rendering engine.
*/
enableIvy?: boolean;
/**
* When true, includes styles inline in the component TS file. By default, an external
* styles file is created and referenced in the component TS file.
*/
inlineStyle?: boolean;
/**
* When true, includes template inline in the component TS file. By default, an external
* template file is created and referenced in the component TS file.
*/
inlineTemplate?: boolean;
/**
* When true, links the CLI to the global version (internal development only).
*/
linkCli?: boolean;
/**
* When true, creates a project without any testing frameworks. (Use for learning purposes
* only.)
*/
minimal?: boolean;
/**
* The name of the new workspace and initial project.
*/
name: string;
/**
* The path where new projects will be created, relative to the new workspace root.
*/
newProjectRoot?: string;
/**
* The prefix to apply to generated selectors for the initial project.
*/
prefix?: string;
/**
* When true, generates a routing module for the initial project.
*/
routing?: boolean;
/**
* When true, does not initialize a git repository.
*/
skipGit?: boolean;
/**
* When true, does not install dependency packages.
*/
skipInstall?: boolean;
/**
* When true, does not generate "spec.ts" test files for the new project.
*/
skipTests?: boolean;
/**
* The file extension or preprocessor to use for style files.
*/
style?: Style;
/**
* The version of the Angular CLI to use.
*/
version: string;
/**
* The view encapsulation strategy to use in the initial project.
*/
viewEncapsulation?: ViewEncapsulation;
}
/**
* Initial git repository commit information.
*/
export declare type CommitUnion = boolean | CommitObject;
export interface CommitObject {
email: string;
message?: string;
name: string;
}
/**
* The file extension or preprocessor to use for style files.
*/
export declare enum Style {
Css = 'css',
Sass = 'sass',
Scss = 'scss',
}
/**
* The view encapsulation strategy to use in the initial project.
*/
export declare enum ViewEncapsulation {
Emulated = 'Emulated',
None = 'None',
ShadowDom = 'ShadowDom'
}

View File

@ -1,65 +0,0 @@
/**
* @license
* Copyright Google LLC 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 {JsonAstNode, JsonAstObject, JsonValue} from '@angular-devkit/core';
import {UpdateRecorder} from '@angular-devkit/schematics';
import {findPropertyInAstObject} from '@schematics/angular/utility/json-utils';
/**
* Replace the value of the key-value pair in the 'node' object with a different
* 'value' and record the update using the specified 'recorder'.
*/
export function replacePropertyInAstObject(
recorder: UpdateRecorder, node: JsonAstObject, propertyName: string, value: JsonValue,
indent: number = 0) {
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);
}
/**
* Remove the key-value pair with the specified 'key' in the specified 'node'
* object and record the update using the specified 'recorder'.
*/
export function removeKeyValueInAstObject(
recorder: UpdateRecorder, content: string, node: JsonAstObject, key: string) {
for (const [i, prop] of node.properties.entries()) {
if (prop.key.value === key) {
const start = prop.start.offset;
const end = prop.end.offset;
let length = end - start;
const match = content.slice(end).match(/^[,\s]+/);
if (match) {
length += match.pop()!.length;
}
recorder.remove(start, length);
if (i === node.properties.length - 1) { // last property
let offset = 0;
while (/(,|\s)/.test(content.charAt(start - offset - 1))) {
offset++;
}
recorder.remove(start - offset, offset);
}
return;
}
}
}
/**
* Returns true if the specified 'node' is a JsonAstObject, false otherwise.
*/
export function isJsonAstObject(node: JsonAstNode|null): node is JsonAstObject {
return !!node && node.kind === 'object';
}

View File

@ -1,110 +0,0 @@
/**
* @license
* Copyright Google LLC 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 {JsonAstObject, parseJsonAst} from '@angular-devkit/core';
import {HostTree} from '@angular-devkit/schematics';
import {UnitTestTree} from '@angular-devkit/schematics/testing';
import {isJsonAstObject, removeKeyValueInAstObject, replacePropertyInAstObject} from './json-utils';
describe('JsonUtils', () => {
let tree: UnitTestTree;
beforeEach(() => {
tree = new UnitTestTree(new HostTree());
});
describe('replacePropertyInAstObject', () => {
it('should replace property', () => {
const content = JSON.stringify({foo: {bar: 'baz'}});
tree.create('tmp', content);
const ast = parseJsonAst(content) as JsonAstObject;
const recorder = tree.beginUpdate('tmp');
replacePropertyInAstObject(recorder, ast, 'foo', [1, 2, 3]);
tree.commitUpdate(recorder);
const value = tree.readContent('tmp');
expect(JSON.parse(value)).toEqual({
foo: [1, 2, 3],
});
expect(value).toBe(`{"foo":[
1,
2,
3
]}`);
});
it('should respect the indent parameter', () => {
const content = JSON.stringify({hello: 'world'}, null, 2);
tree.create('tmp', content);
const ast = parseJsonAst(content) as JsonAstObject;
const recorder = tree.beginUpdate('tmp');
replacePropertyInAstObject(recorder, ast, 'hello', 'world!', 2);
tree.commitUpdate(recorder);
const value = tree.readContent('tmp');
expect(JSON.parse(value)).toEqual({
hello: 'world!',
});
expect(value).toBe(`{
"hello": "world!"
}`);
});
it('should throw error if property is not found', () => {
const content = JSON.stringify({});
tree.create('tmp', content);
const ast = parseJsonAst(content) as JsonAstObject;
const recorder = tree.beginUpdate('tmp');
expect(() => replacePropertyInAstObject(recorder, ast, 'foo', 'bar'))
.toThrowError(`Property 'foo' does not exist in JSON object`);
});
});
describe('removeKeyValueInAstObject', () => {
it('should remove key-value pair', () => {
const content = JSON.stringify({hello: 'world', foo: 'bar'});
tree.create('tmp', content);
const ast = parseJsonAst(content) as JsonAstObject;
let recorder = tree.beginUpdate('tmp');
removeKeyValueInAstObject(recorder, content, ast, 'foo');
tree.commitUpdate(recorder);
const tmp = tree.readContent('tmp');
expect(JSON.parse(tmp)).toEqual({
hello: 'world',
});
expect(tmp).toBe('{"hello":"world"}');
recorder = tree.beginUpdate('tmp');
const newContent = tree.readContent('tmp');
removeKeyValueInAstObject(recorder, newContent, ast, 'hello');
tree.commitUpdate(recorder);
const value = tree.readContent('tmp');
expect(JSON.parse(value)).toEqual({});
expect(value).toBe('{}');
});
it('should be a noop if key is not found', () => {
const content = JSON.stringify({foo: 'bar'});
tree.create('tmp', content);
const ast = parseJsonAst(content) as JsonAstObject;
let recorder = tree.beginUpdate('tmp');
expect(() => removeKeyValueInAstObject(recorder, content, ast, 'hello')).not.toThrow();
tree.commitUpdate(recorder);
const value = tree.readContent('tmp');
expect(JSON.parse(value)).toEqual({foo: 'bar'});
expect(value).toBe('{"foo":"bar"}');
});
});
describe('isJsonAstObject', () => {
it('should return true for an object', () => {
const ast = parseJsonAst(JSON.stringify({}));
expect(isJsonAstObject(ast)).toBe(true);
});
it('should return false for a non-object', () => {
const ast = parseJsonAst(JSON.stringify([]));
expect(isJsonAstObject(ast)).toBe(false);
});
});
});

View File

@ -1,40 +0,0 @@
/**
* @license
* Copyright Google LLC 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 {JsonAstNode, JsonAstObject} from '@angular-devkit/core';
import {findPropertyInAstObject} from '@schematics/angular/utility/json-utils';
import {isJsonAstObject} from './json-utils';
/**
* Find the e2e architect node in the JSON ast.
* The e2e application is relocated alongside the existing application.
* This function supports looking up the e2e architect in both the new and old
* layout.
* See https://github.com/angular/angular-cli/pull/13780
*/
export function findE2eArchitect(ast: JsonAstObject, name: string): JsonAstObject|null {
const projects = findPropertyInAstObject(ast, 'projects');
if (!isJsonAstObject(projects)) {
return null;
}
let architect: JsonAstNode|null;
const e2e = findPropertyInAstObject(projects, `${name}-e2e`);
if (isJsonAstObject(e2e)) {
architect = findPropertyInAstObject(e2e, 'architect');
} else {
const project = findPropertyInAstObject(projects, name);
if (!isJsonAstObject(project)) {
return null;
}
architect = findPropertyInAstObject(project, 'architect');
}
if (!isJsonAstObject(architect)) {
return null;
}
return architect;
}

View File

@ -1,42 +0,0 @@
/**
* @license
* Copyright Google LLC 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 {JsonAstObject, parseJsonAst} from '@angular-devkit/core';
import {isJsonAstObject} from './json-utils';
import {findE2eArchitect} from './workspace-utils';
describe('Workspace utils', () => {
describe('findE2eArchitect', () => {
it('should find e2e architect in old project layout', () => {
const workspace = {
projects: {
demo: {},
'demo-e2e': {
architect: {},
},
},
};
const ast = parseJsonAst(JSON.stringify(workspace));
const architect = findE2eArchitect(ast as JsonAstObject, 'demo');
expect(isJsonAstObject(architect)).toBe(true);
});
it('should find e2e architect in new project layout', () => {
const workspace = {
projects: {
demo: {
architect: {},
},
},
};
const ast = parseJsonAst(JSON.stringify(workspace));
const architect = findE2eArchitect(ast as JsonAstObject, 'demo');
expect(isJsonAstObject(architect)).toBe(true);
});
});
});