feat(schematics): compatibility with Angular CLI 6.2.0 (#13078)

* Upgrades the devkit dependencies to the latest versions.
* Adds support for the newly introduced and published `project.targets` instead of `project.architect` (see: https://github.com/angular/angular-cli/commit/307160806cb48c95ecb8982854f452303801ac9f)
* Compatibility behavior for a published but non-announced breaking change within the schematic context (see: https://github.com/angular/angular-cli/commit/9720077a4b5ae628ddfaf5f1a26bac3b04bdf637#diff-e73df838d6700d187eff76a50c7e1c53R233`)
* Supports the new `project.targets` schema within the update schematics (in order to properly determine `tsconfig` files)
* Due to the fact that the @angular-devkit/schematics now handles Bazel better, the template files will be resolved through NodeJS. This means that the template files need to be referenced in the @//:node_modules label.

Fixes #11438
This commit is contained in:
Paul Gschwendtner
2018-09-12 00:13:35 +02:00
committed by Jeremy Elbourn
parent 6b2f00897f
commit 714c205312
15 changed files with 163 additions and 149 deletions
+3
View File
@@ -38,6 +38,9 @@ filegroup(
"*.d.ts",
]] + [
"node_modules/http-server/**",
# Reference all files of the "@schematics/angular" package because the schematic
# tests depend on the template files of the angular schematics.
"node_modules/@schematics/angular/**",
]),
)
+16 -23
View File
@@ -5,14 +5,15 @@
"requires": true,
"dependencies": {
"@angular-devkit/core": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.7.4.tgz",
"integrity": "sha512-Blh44vzZVzE8B9xIwjRoo7hXPGSDdlrrax0rntvt3DDGVTjsSGm43qT95aDmXiwJruOCJNC5DsaP3+tTAkAyQQ==",
"version": "0.9.0-beta.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-0.9.0-beta.2.tgz",
"integrity": "sha512-zs6kCtFSg1GBqC0Jq0nxndX43Tp1IphwCF8PCy2airLLs15OTzZ0lRqDy5hSzKWy6WsY3X6yUXXmSe+5fO6SiQ==",
"dev": true,
"requires": {
"ajv": "~6.4.0",
"chokidar": "^2.0.3",
"rxjs": "^6.0.0",
"fast-json-stable-stringify": "^2.0.0",
"rxjs": "~6.2.0",
"source-map": "^0.5.6"
},
"dependencies": {
@@ -25,13 +26,13 @@
}
},
"@angular-devkit/schematics": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.7.4.tgz",
"integrity": "sha512-Gkm2mBMm6a0mNKqZsAcS42VaO7zNSIXhIKbMyZQIcQ1ZMwbsx+Rs0dliQwFVfEVCNGc1pjh0Idc5O5V/g/N5Fw==",
"version": "0.9.0-beta.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-0.9.0-beta.2.tgz",
"integrity": "sha512-+4wJ7tTmzzTKxu8EUdIzytmDddeUNeUFfakqz7lG6Go5rGJCOO+GMY4oLPl8OAbE4kGv8Psz8DDP1xf/emlX1Q==",
"dev": true,
"requires": {
"@angular-devkit/core": "0.7.4",
"rxjs": "^6.0.0"
"@angular-devkit/core": "0.9.0-beta.2",
"rxjs": "~6.2.0"
}
},
"@angular/animations": {
@@ -1027,22 +1028,14 @@
"dev": true
},
"@schematics/angular": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.7.4.tgz",
"integrity": "sha512-/KJH5fPv+40VdadWmVnXR2GlvFZKYH01eYw7XOi77gXM896gk2tGlWmm+6RjUaVyec49ivusmmhRJEjiTyA7NA==",
"version": "0.9.0-beta.2",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.9.0-beta.2.tgz",
"integrity": "sha512-+IR5m3tVFSpyjSj0avWr9oKEBbHgstZZ7lfsmaNwrpNRxiQVlERGS0qLWJYLiOiMBQmJlss71lQAXtug8i1a/g==",
"dev": true,
"requires": {
"@angular-devkit/core": "0.7.4",
"@angular-devkit/schematics": "0.7.4",
"typescript": ">=2.6.2 <2.10"
},
"dependencies": {
"typescript": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz",
"integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==",
"dev": true
}
"@angular-devkit/core": "0.9.0-beta.2",
"@angular-devkit/schematics": "0.9.0-beta.2",
"typescript": ">=2.6.2 <3.1"
}
},
"@sindresorhus/is": {
+3 -3
View File
@@ -44,8 +44,8 @@
"zone.js": "^0.8.26"
},
"devDependencies": {
"@angular-devkit/core": "0.7.4",
"@angular-devkit/schematics": "0.7.4",
"@angular-devkit/core": "^0.9.0-beta.2",
"@angular-devkit/schematics": "^0.9.0-beta.2",
"@angular/bazel": "7.0.0-beta.4",
"@angular/compiler-cli": "7.0.0-beta.4",
"@angular/http": "7.0.0-beta.4",
@@ -56,7 +56,7 @@
"@bazel/ibazel": "0.3.1",
"@google-cloud/storage": "^1.1.1",
"@octokit/rest": "^15.9.4",
"@schematics/angular": "0.7.4",
"@schematics/angular": "^0.9.0-beta.2",
"@types/chalk": "^0.4.31",
"@types/fs-extra": "^4.0.3",
"@types/glob": "^5.0.33",
@@ -8,11 +8,11 @@
import {SchematicsException} from '@angular-devkit/schematics';
import {WorkspaceProject} from '@schematics/angular/utility/config';
import {getArchitectOptions} from '../../utils/architect-options';
import {getProjectTargetOptions} from '../../utils/project-targets';
/** Looks for the index HTML file in the given project and returns its path. */
export function getIndexHtmlPath(project: WorkspaceProject): string {
const buildOptions = getArchitectOptions(project, 'build');
const buildOptions = getProjectTargetOptions(project, 'build');
if (!buildOptions.index) {
throw new SchematicsException('No project "index.html" file could be found.');
+2 -5
View File
@@ -1,5 +1,6 @@
import {Tree} from '@angular-devkit/schematics';
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
import {getProjectTargetOptions} from '@angular/material/schematics/utils/project-targets';
import {getProjectStyleFile} from '../utils/project-style-file';
import {getIndexHtmlPath} from './fonts/project-index-html';
import {getProjectFromWorkspace} from '../utils/get-project';
@@ -19,11 +20,7 @@ describe('material-install-schematic', () => {
/** Expects the given file to be in the styles of the specified workspace project. */
function expectProjectStyleFile(project: WorkspaceProject, filePath: string) {
const architect = project.architect!;
expect(architect!['build']).toBeTruthy();
expect(architect!['build']['options']).toBeTruthy();
expect(architect!['build']['options']['styles']).toContain(filePath,
expect(getProjectTargetOptions(project, 'build').styles).toContain(filePath,
`Expected "${filePath}" to be added to the project styles in the workspace.`);
}
+28 -27
View File
@@ -13,6 +13,7 @@ import {getWorkspace, WorkspaceProject, WorkspaceSchema} from '@schematics/angul
import {join} from 'path';
import {getProjectFromWorkspace} from '../../utils/get-project';
import {getProjectStyleFile} from '../../utils/project-style-file';
import {getProjectTargetOptions} from '../../utils/project-targets';
import {Schema} from '../schema';
import {createCustomTheme} from './custom-theme';
@@ -60,9 +61,7 @@ function insertCustomTheme(project: WorkspaceProject, projectName: string, host:
host.create(customThemePath, themeContent);
// Architect is always defined because we initially asserted if the default builder
// configuration is set up or not.
return addStyleToTarget(project.architect!['build'], host, customThemePath, workspace);
return addStyleToTarget(project, 'build', host, customThemePath, workspace);
}
const insertion = new InsertChange(stylesPath, 0, themeContent);
@@ -79,27 +78,24 @@ function insertPrebuiltTheme(project: WorkspaceProject, host: Tree, theme: strin
// Path needs to be always relative to the `package.json` or workspace root.
const themePath = `./node_modules/@angular/material/prebuilt-themes/${theme}.css`;
// Architect is always defined because we initially asserted if the default builder
// configuration is set up or not.
addStyleToTarget(project.architect!['build'], host, themePath, workspace);
addStyleToTarget(project.architect!['test'], host, themePath, workspace);
addStyleToTarget(project, 'build', host, themePath, workspace);
addStyleToTarget(project, 'test', host, themePath, workspace);
}
/** Adds a style entry to the given target. */
function addStyleToTarget(target: any, host: Tree, asset: string, workspace: WorkspaceSchema) {
// We can't assume that any of these properties are defined, so safely add them as we go
// if necessary.
if (!target.options) {
target.options = {styles: [asset]};
} else if (!target.options.styles) {
target.options.styles = [asset];
/** Adds a style entry to the given project target. */
function addStyleToTarget(project: WorkspaceProject, targetName: string, host: Tree,
assetPath: string, workspace: WorkspaceSchema) {
const targetOptions = getProjectTargetOptions(project, targetName);
if (!targetOptions.styles) {
targetOptions.styles = [assetPath];
} else {
const existingStyles = target.options.styles.map(s => typeof s === 'string' ? s : s.input);
const hasGivenTheme = existingStyles.find(s => s.includes(asset));
const existingStyles = targetOptions.styles.map(s => typeof s === 'string' ? s : s.input);
const hasGivenTheme = existingStyles.find(s => s.includes(assetPath));
const hasOtherTheme = existingStyles.find(s => s.includes('material/prebuilt'));
if (!hasGivenTheme && !hasOtherTheme) {
target.options.styles.unshift(asset);
targetOptions.styles.unshift(assetPath);
}
}
@@ -108,18 +104,23 @@ function addStyleToTarget(target: any, host: Tree, asset: string, workspace: Wor
/** Throws if the project is not using the default Angular devkit builders. */
function assertDefaultBuildersConfigured(project: WorkspaceProject) {
const defaultBuilder = '@angular-devkit/build-angular:browser';
const defaultTestBuilder = '@angular-devkit/build-angular:karma';
checkProjectTargetBuilder(project, 'build', '@angular-devkit/build-angular:browser');
checkProjectTargetBuilder(project, 'test', '@angular-devkit/build-angular:karma');
}
const hasDefaultBuilders = project.architect &&
project.architect['build'] &&
project.architect['build']['builder'] === defaultBuilder &&
project.architect['test'] &&
project.architect['test']['builder'] === defaultTestBuilder;
/**
* Checks if the specified project target is configured with the default builders which are
* provided by the Angular CLI.
*/
function checkProjectTargetBuilder(project: WorkspaceProject, targetName: string,
defaultBuilder: string) {
if (!hasDefaultBuilders) {
const targetConfig = project.architect && project.architect[targetName] ||
project.targets && project.targets[targetName];
if (!targetConfig || targetConfig['builder'] !== defaultBuilder) {
throw new SchematicsException(
'Your project is not using the default builders for build and test. The Angular Material ' +
`Your project is not using the default builders for "${targetName}". The Angular Material ` +
'schematics can only be used if the original builders from the Angular CLI are configured.');
}
}
@@ -0,0 +1,47 @@
/**
* @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 {Tree} from '@angular-devkit/schematics';
import {getWorkspace} from '@schematics/angular/utility/config';
/**
* Gets all tsconfig paths from a CLI project by reading the workspace configuration
* and looking for common tsconfig locations.
*/
export function getProjectTsConfigPaths(tree: Tree): string[] {
// Start with some tsconfig paths that are generally used within CLI projects.
const tsconfigPaths = new Set<string>([
'./tsconfig.json',
'./src/tsconfig.json',
'./src/tsconfig.app.json',
]);
// Add any tsconfig directly referenced in a build or test task of the angular.json workspace.
const workspace = getWorkspace(tree);
for (const project of Object.values(workspace.projects)) {
['build', 'test'].forEach(targetName => {
if (project.targets &&
project.targets[targetName] &&
project.targets[targetName].options &&
project.targets[targetName].options.tsConfig) {
tsconfigPaths.add(project.targets[targetName].options.tsConfig);
}
if (project.architect &&
project.architect[targetName] &&
project.architect[targetName].options &&
project.architect[targetName].options.tsConfig) {
tsconfigPaths.add(project.architect[targetName].options.tsConfig);
}
});
}
// Filter out tsconfig files that don't exist in the CLI project.
return Array.from(tsconfigPaths).filter(p => tree.exists(p));
}
+4 -41
View File
@@ -8,25 +8,25 @@
import {Rule, SchematicContext, TaskId, Tree} from '@angular-devkit/schematics';
import {RunSchematicTask, TslintFixTask} from '@angular-devkit/schematics/tasks';
import {getWorkspace} from '@schematics/angular/utility/config';
import {TargetVersion} from './index';
import {getProjectTsConfigPaths} from './project-tsconfig-paths';
import {createTslintConfig} from './tslint-update';
/** Entry point for `ng update` from Angular CLI. */
export function createUpdateRule(targetVersion: TargetVersion): Rule {
return (tree: Tree, context: SchematicContext) => {
const allTsConfigPaths = getTsConfigPaths(tree);
const projectTsConfigPaths = getProjectTsConfigPaths(tree);
const tslintFixTasks: TaskId[] = [];
if (!allTsConfigPaths.length) {
if (!projectTsConfigPaths.length) {
throw new Error('Could not find any tsconfig file. Please submit an issue on the Angular ' +
'Material repository that includes the name of your TypeScript configuration.');
}
const tslintConfig = createTslintConfig(targetVersion);
for (const tsconfig of allTsConfigPaths) {
for (const tsconfig of projectTsConfigPaths) {
// Run the update tslint rules.
tslintFixTasks.push(context.addTask(new TslintFixTask(tslintConfig, {
silent: false,
@@ -39,40 +39,3 @@ export function createUpdateRule(targetVersion: TargetVersion): Rule {
context.addTask(new RunSchematicTask('ng-post-update', {}), tslintFixTasks);
};
}
/**
* Gets all tsconfig paths from a CLI project by reading the workspace configuration
* and looking for common tsconfig locations.
*/
function getTsConfigPaths(tree: Tree): string[] {
// Start with some tsconfig paths that are generally used.
const tsconfigPaths = [
'./tsconfig.json',
'./src/tsconfig.json',
'./src/tsconfig.app.json',
];
// Add any tsconfig directly referenced in a build or test task of the angular.json workspace.
const workspace = getWorkspace(tree);
for (const project of Object.values(workspace.projects)) {
if (project && project.architect) {
for (const taskName of ['build', 'test']) {
const task = project.architect[taskName];
if (task && task.options && task.options.tsConfig) {
const tsConfigOption = task.options.tsConfig;
if (typeof tsConfigOption === 'string') {
tsconfigPaths.push(tsConfigOption);
} else if (Array.isArray(tsConfigOption)) {
tsconfigPaths.push(...tsConfigOption);
}
}
}
}
}
// Filter out tsconfig files that don't exist and remove any duplicates.
return tsconfigPaths
.filter(p => tree.exists(p))
.filter((value, index, self) => self.indexOf(value) === index);
}
@@ -1,21 +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 {WorkspaceProject} from '@schematics/angular/utility/config';
/** Resolves the architect options for the build target of the given project. */
export function getArchitectOptions(project: WorkspaceProject, buildTarget: string) {
if (project.architect &&
project.architect[buildTarget] &&
project.architect[buildTarget].options) {
return project.architect[buildTarget].options;
}
throw new Error(`Cannot determine architect configuration for target: ${buildTarget}.`);
}
+1 -4
View File
@@ -49,10 +49,7 @@ export function addModuleImportToModule(host: Tree, modulePath: string, moduleNa
throw new SchematicsException(`Module not found: ${modulePath}`);
}
// TODO(devversion): Cast to any because the Bazel typescript rules seem to incorrectly resolve
// the the required TypeScript version for the @schematics/angular utility functions. Meaning
// that is a type signature mismatch at compilation which is not valid.
const changes = addImportToModule(moduleSource as any, modulePath, moduleName, src);
const changes = addImportToModule(moduleSource, modulePath, moduleName, src);
const recorder = host.beginUpdate(modulePath);
changes.forEach((change) => {
+15 -17
View File
@@ -36,7 +36,7 @@ import {buildRelativePath, findModuleFromOptions} from '@schematics/angular/util
import {parseName} from '@schematics/angular/utility/parse-name';
import {buildDefaultPath} from '@schematics/angular/utility/project';
import {validateHtmlSelector, validateName} from '@schematics/angular/utility/validation';
import {readFileSync} from 'fs';
import {readFileSync, statSync} from 'fs';
import {dirname, join, resolve} from 'path';
import {getProjectFromWorkspace} from './get-project';
import {getDefaultComponentOptions} from './schematic-options';
@@ -67,11 +67,8 @@ function addDeclarationToNgModule(options: ComponentOptions): Rule {
const relativePath = buildRelativePath(modulePath, componentPath);
const classifiedName = strings.classify(`${options.name}Component`);
// TODO(devversion): Cast to any because the Bazel typescript rules seem to incorrectly resolve
// the the required TypeScript version for the @schematics/angular utility functions. Meaning
// that is a type signature mismatch at compilation which is not valid.
const declarationChanges = addDeclarationToModule(
source as any,
source,
modulePath,
classifiedName,
relativePath);
@@ -89,11 +86,8 @@ function addDeclarationToNgModule(options: ComponentOptions): Rule {
const source = readIntoSourceFile(host, modulePath);
const exportRecorder = host.beginUpdate(modulePath);
// TODO(devversion): Cast to any because the Bazel typescript rules seem to incorrectly resolve
// the the required TypeScript version for the @schematics/angular utility functions. Meaning
// that is a type signature mismatch at compilation which is not valid.
const exportChanges = addExportToModule(
source as any,
source,
modulePath,
strings.classify(`${options.name}Component`),
relativePath);
@@ -111,11 +105,8 @@ function addDeclarationToNgModule(options: ComponentOptions): Rule {
const source = readIntoSourceFile(host, modulePath);
const entryComponentRecorder = host.beginUpdate(modulePath);
// TODO(devversion): Cast to any because the Bazel typescript rules seem to incorrectly resolve
// the the required TypeScript version for the @schematics/angular utility functions. Meaning
// that is a type signature mismatch at compilation which is not valid.
const entryComponentChanges = addEntryComponentToModule(
source as any,
source,
modulePath,
strings.classify(`${options.name}Component`),
relativePath);
@@ -172,9 +163,16 @@ export function buildComponent(options: ComponentOptions,
const project = getProjectFromWorkspace(workspace, options.project);
const defaultComponentOptions = getDefaultComponentOptions(project);
// TODO(devversion): Remove if we drop support for older CLI versions.
// This handles an unreported breaking change from the @angular-devkit/schematics. Previously
// the description path resolved to the factory file, but starting from 6.2.0, it resolves
// to the factory directory.
const schematicPath = statSync(context.schematic.description.path).isDirectory() ?
context.schematic.description.path :
dirname(context.schematic.description.path);
const schematicFilesUrl = './files';
const schematicFilesPath = resolve(dirname(context.schematic.description.path),
schematicFilesUrl);
const schematicFilesPath = resolve(schematicPath, schematicFilesUrl);
// Add the default component option values to the options if an option is not explicitly
// specified but a default component option is available.
@@ -188,14 +186,14 @@ export function buildComponent(options: ComponentOptions,
options.module = findModuleFromOptions(host, options);
const parsedPath = parseName(options.path, options.name);
const parsedPath = parseName(options.path!, options.name);
options.name = parsedPath.name;
options.path = parsedPath.path;
options.selector = options.selector || buildSelector(options, project.prefix);
validateName(options.name);
validateHtmlSelector(options.selector);
validateHtmlSelector(options.selector!);
// Object that will be used as context for the EJS templates.
const baseTemplateContext = {
@@ -8,11 +8,11 @@
import {SchematicsException} from '@angular-devkit/schematics';
import {WorkspaceProject} from '@schematics/angular/utility/config';
import {getArchitectOptions} from './architect-options';
import {getProjectTargetOptions} from './project-targets';
/** Looks for the main TypeScript file in the given project and returns its path. */
export function getProjectMainFile(project: WorkspaceProject): string {
const buildOptions = getArchitectOptions(project, 'build');
const buildOptions = getProjectTargetOptions(project, 'build');
if (!buildOptions.main) {
throw new SchematicsException(`Could not find the project main file inside of the ` +
@@ -8,7 +8,7 @@
import {normalize} from '@angular-devkit/core';
import {WorkspaceProject} from '@schematics/angular/utility/config';
import {getArchitectOptions} from './architect-options';
import {getProjectTargetOptions} from './project-targets';
/** Regular expression that matches all possible Angular CLI default style files. */
const defaultStyleFileRegex = /styles\.(c|le|sc)ss/;
@@ -21,7 +21,7 @@ const validStyleFileRegex = /\.(c|le|sc)ss/;
* extension is specified, any style file with a valid extension will be returned.
*/
export function getProjectStyleFile(project: WorkspaceProject, extension?: string): string | null {
const buildOptions = getArchitectOptions(project, 'build');
const buildOptions = getProjectTargetOptions(project, 'build');
if (buildOptions.styles && buildOptions.styles.length) {
const styles = buildOptions.styles.map(s => typeof s === 'string' ? s : s.input);
@@ -0,0 +1,31 @@
/**
* @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 {WorkspaceProject} from '@schematics/angular/utility/config';
/** Resolves the architect options for the build target of the given project. */
export function getProjectTargetOptions(project: WorkspaceProject, buildTarget: string) {
if (project.targets &&
project.targets[buildTarget] &&
project.targets[buildTarget].options) {
return project.targets[buildTarget].options;
}
// TODO(devversion): consider removing this architect check if the CLI completely switched
// over to `targets`, and the `architect` support has been removed.
// See: https://github.com/angular/angular-cli/commit/307160806cb48c95ecb8982854f452303801ac9f
if (project.architect &&
project.architect[buildTarget] &&
project.architect[buildTarget].options) {
return project.architect[buildTarget].options;
}
throw new Error(`Cannot determine project target configuration for: ${buildTarget}.`);
}
@@ -6,8 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
/** This is just a type import and won't be generated in the release output. */
import typescript = require('@schematics/angular/node_modules/typescript');
/**
* This is just a type import and won't be generated in the release output.
*
* Note that we always need to adjust this type import based on the location of the Typescript
* dependency that will be shipped with `@schematics/angular`.
*/
import typescript = require('typescript');
/**
* This is an agnostic re-export of TypeScript. Depending on the context, this module file will