397 lines
14 KiB
TypeScript
Executable File
397 lines
14 KiB
TypeScript
Executable File
/**
|
|
* @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 {JsonAstObject, parseJsonAst} from '@angular-devkit/core';
|
|
import {Rule, SchematicContext, SchematicsException, Tree, apply, applyTemplates, chain, mergeWith, url} from '@angular-devkit/schematics';
|
|
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
|
|
import {getWorkspace, getWorkspacePath} from '@schematics/angular/utility/config';
|
|
import {findPropertyInAstObject, insertPropertyInAstObjectInOrder} from '@schematics/angular/utility/json-utils';
|
|
import {validateProjectName} from '@schematics/angular/utility/validation';
|
|
|
|
import {isJsonAstObject, removeKeyValueInAstObject, 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 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,
|
|
'@bazel/bazel': '^0.26.0-rc.5',
|
|
'@bazel/ibazel': '^0.10.2',
|
|
'@bazel/karma': '0.27.12',
|
|
'@bazel/typescript': '0.27.12',
|
|
};
|
|
|
|
const recorder = host.beginUpdate(packageJson);
|
|
for (const packageName of Object.keys(devDependencies)) {
|
|
const existingDep = findPropertyInAstObject(deps, packageName);
|
|
if (existingDep) {
|
|
const content = packageJsonContent.toString();
|
|
removeKeyValueInAstObject(recorder, content, deps, packageName);
|
|
}
|
|
const version = devDependencies[packageName];
|
|
const indent = 4;
|
|
if (findPropertyInAstObject(devDeps, packageName)) {
|
|
replacePropertyInAstObject(recorder, devDeps, packageName, version, indent);
|
|
} else {
|
|
insertPropertyInAstObjectInOrder(recorder, devDeps, packageName, version, indent);
|
|
}
|
|
}
|
|
host.commitUpdate(recorder);
|
|
return host;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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, context: SchematicContext) => {
|
|
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/...'},
|
|
},
|
|
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));
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a backup for the original tsconfig.json file in case user wants to
|
|
* eject Bazel and revert to the original workflow.
|
|
*/
|
|
function backupTsconfigJson(): Rule {
|
|
return (host: Tree, context: SchematicContext) => {
|
|
const tsconfigPath = 'tsconfig.json';
|
|
if (!host.exists(tsconfigPath)) {
|
|
return;
|
|
}
|
|
host.create(
|
|
`${tsconfigPath}.bak`, '// This is a backup file of the original tsconfig.json. ' +
|
|
'This file is needed in case you want to revert to the workflow without Bazel.\n\n' +
|
|
host.read(tsconfigPath));
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Bazel controls the compilation options of tsc, so many options in
|
|
* tsconfig.json generated by the default CLI schematics are not applicable.
|
|
* This function updates the tsconfig.json to remove Bazel-controlled
|
|
* parameters. This prevents Bazel from printing out warnings about overriden
|
|
* settings.
|
|
*/
|
|
function updateTsconfigJson(): Rule {
|
|
return (host: Tree, context: SchematicContext) => {
|
|
const tsconfigPath = 'tsconfig.json';
|
|
if (!host.exists(tsconfigPath)) {
|
|
return host;
|
|
}
|
|
const contentRaw = host.read(tsconfigPath) !.toString();
|
|
if (!contentRaw) {
|
|
return host;
|
|
}
|
|
const content = contentRaw.toString();
|
|
const ast = parseJsonAst(content);
|
|
if (!isJsonAstObject(ast)) {
|
|
return host;
|
|
}
|
|
const compilerOptions = findPropertyInAstObject(ast, 'compilerOptions');
|
|
if (!isJsonAstObject(compilerOptions)) {
|
|
return host;
|
|
}
|
|
const recorder = host.beginUpdate(tsconfigPath);
|
|
// target and module are controlled by downstream dependencies, such as
|
|
// ts_devserver
|
|
removeKeyValueInAstObject(recorder, content, compilerOptions, 'target');
|
|
removeKeyValueInAstObject(recorder, content, compilerOptions, 'module');
|
|
// typeRoots is always set to the @types subdirectory of the node_modules
|
|
// attribute
|
|
removeKeyValueInAstObject(recorder, content, compilerOptions, 'typeRoots');
|
|
// rootDir and baseUrl are always the workspace root directory
|
|
removeKeyValueInAstObject(recorder, content, compilerOptions, 'rootDir');
|
|
removeKeyValueInAstObject(recorder, content, compilerOptions, 'baseUrl');
|
|
host.commitUpdate(recorder);
|
|
return host;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @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 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 deps = findPropertyInAstObject(jsonAst, 'dependencies');
|
|
if (!isJsonAstObject(deps)) {
|
|
throw new Error(`Failed to find dependencies in ${packageJson}`);
|
|
}
|
|
const rxjs = findPropertyInAstObject(deps, 'rxjs');
|
|
if (!rxjs) {
|
|
throw new Error(`Failed to find rxjs in dependencies of ${packageJson}`);
|
|
}
|
|
const value = rxjs.value as string; // value can be version or range
|
|
const match = value.match(/(\d)+\.(\d)+.(\d)+$/);
|
|
if (match) {
|
|
const [_, major, minor] = match;
|
|
if (major < '6' || (major === '6' && minor < '4')) {
|
|
const recorder = host.beginUpdate(packageJson);
|
|
replacePropertyInAstObject(recorder, deps, 'rxjs', '~6.4.0');
|
|
host.commitUpdate(recorder);
|
|
}
|
|
} else {
|
|
context.logger.info(
|
|
'Could not determine version of rxjs. \n' +
|
|
'Please make sure that version is at least 6.4.0.');
|
|
}
|
|
return host;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* When using Angular NPM packages and building with AOT compilation, ngc
|
|
* requires ngsumamry files but they are not shipped. This function adds a
|
|
* postinstall step to generate these files.
|
|
*/
|
|
function addPostinstallToGenerateNgSummaries() {
|
|
return (host: Tree, context: SchematicContext) => {
|
|
if (!host.exists('angular-metadata.tsconfig.json')) {
|
|
return;
|
|
}
|
|
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);
|
|
if (scripts) {
|
|
insertPropertyInAstObjectInOrder(
|
|
recorder, scripts, 'postinstall', 'ngc -p ./angular-metadata.tsconfig.json', 4);
|
|
} else {
|
|
insertPropertyInAstObjectInOrder(
|
|
recorder, jsonAst, 'scripts', {
|
|
postinstall: 'ngc -p ./angular-metadata.tsconfig.json',
|
|
},
|
|
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),
|
|
addPostinstallToGenerateNgSummaries(),
|
|
backupAngularJson(),
|
|
backupTsconfigJson(),
|
|
updateAngularJsonToUseBazelBuilder(options),
|
|
updateGitignore(),
|
|
updateTsconfigJson(),
|
|
upgradeRxjs(),
|
|
installNodeModules(options),
|
|
]);
|
|
};
|
|
}
|