From 1c26f40cd40506e84f4160cdb7184dfdd5450603 Mon Sep 17 00:00:00 2001 From: Walter Werner SCHNEIDER Date: Tue, 5 May 2020 12:47:12 +0300 Subject: [PATCH] refactor(localize): use the new workspaces API for ng-add schematic (#36897) Updates the @angular/localize ng-add schematic to use the new workspaces API and removes dependency on private APIs. PR Close #36897 --- packages/localize/schematics/ng-add/README.md | 2 +- packages/localize/schematics/ng-add/index.ts | 105 +++++++++++------- .../localize/schematics/ng-add/index_spec.ts | 2 + 3 files changed, 66 insertions(+), 43 deletions(-) diff --git a/packages/localize/schematics/ng-add/README.md b/packages/localize/schematics/ng-add/README.md index 4c125ae34b..e6078ad36a 100644 --- a/packages/localize/schematics/ng-add/README.md +++ b/packages/localize/schematics/ng-add/README.md @@ -1,6 +1,6 @@ # @angular/localize schematic for `ng add` -This schematic will be executed when a Angular CLI user runs `ng add @angular/localize`. +This schematic will be executed when an Angular CLI user runs `ng add @angular/localize`. It will search their `angular.json` file, and find polyfills and main files for server builders. Then it will add the `@angular/localize/init` polyfill that `@angular/localize` needs to work. \ No newline at end of file diff --git a/packages/localize/schematics/ng-add/index.ts b/packages/localize/schematics/ng-add/index.ts index 94ec56b5a0..52c459db72 100644 --- a/packages/localize/schematics/ng-add/index.ts +++ b/packages/localize/schematics/ng-add/index.ts @@ -8,55 +8,72 @@ * @fileoverview Schematics for ng-new project that builds with Bazel. */ -import {virtualFs} from '@angular-devkit/core'; -import {chain, Rule, Tree} from '@angular-devkit/schematics'; -import {getWorkspace} from '@schematics/angular/utility/config'; -import {getProjectTargets} from '@schematics/angular/utility/project-targets'; -import {validateProjectName} from '@schematics/angular/utility/validation'; -import {BrowserBuilderTarget, Builders, ServeBuilderTarget} from '@schematics/angular/utility/workspace-models'; +import {virtualFs, workspaces} from '@angular-devkit/core'; +import {chain, Rule, SchematicsException, Tree} from '@angular-devkit/schematics'; +import {getWorkspace} from '@schematics/angular/utility/workspace'; +import {Builders} from '@schematics/angular/utility/workspace-models'; import {Schema} from './schema'; export const localizePolyfill = `import '@angular/localize/init';`; -function getAllOptionValues( - host: Tree, projectName: string, builderName: string, optionName: string) { - const targets = getProjectTargets(host, projectName); - - // Find all targets of a specific build in a project. - const builderTargets: (BrowserBuilderTarget|ServeBuilderTarget)[] = Object.values(targets).filter( - (target: BrowserBuilderTarget|ServeBuilderTarget) => target.builder === builderName); - - // Get all options contained in target configuration partials. - const configurationOptions = builderTargets.filter(t => t.configurations) - .map(t => Object.values(t.configurations!)) - .reduce((acc, cur) => acc.concat(...cur), []); - - // Now we have all option sets. We can use it to find all references to a given property. - const allOptions = [ - ...builderTargets.map(t => t.options), - ...configurationOptions, - ]; - - // Get all values for the option name and dedupe them. - // Deduping will only work for primitives, but the keys we want here are strings so it's ok. - const optionValues: T[] = - allOptions.filter(o => o[optionName]) - .map(o => o[optionName]) - .reduce((acc, cur) => !acc.includes(cur) ? acc.concat(cur) : acc, []); +function getRelevantTargetDefinitions( + project: workspaces.ProjectDefinition, builderName: Builders): workspaces.TargetDefinition[] { + const definitions: workspaces.TargetDefinition[] = []; + project.targets.forEach((target: workspaces.TargetDefinition): void => { + if (target.builder === builderName) { + definitions.push(target); + } + }); + return definitions; +} +function getOptionValuesForTargetDefinition( + definition: workspaces.TargetDefinition, optionName: string): string[] { + const optionValues: string[] = []; + if (definition.options && optionName in definition.options) { + let optionValue: unknown = definition.options[optionName]; + if (typeof optionValue === 'string') { + optionValues.push(optionValue); + } + } + if (!definition.configurations) { + return optionValues; + } + Object.values(definition.configurations) + .forEach((configuration: Record|undefined): void => { + if (configuration && optionName in configuration) { + const optionValue: unknown = configuration[optionName]; + if (typeof optionValue === 'string') { + optionValues.push(optionValue); + } + } + }); return optionValues; } +function getFileListForRelevantTargetDefinitions( + project: workspaces.ProjectDefinition, builderName: Builders, optionName: string): string[] { + const fileList: string[] = []; + const definitions = getRelevantTargetDefinitions(project, builderName); + definitions.forEach((definition: workspaces.TargetDefinition): void => { + const optionValues = getOptionValuesForTargetDefinition(definition, optionName); + optionValues.forEach((filePath: string): void => { + if (fileList.indexOf(filePath) === -1) { + fileList.push(filePath); + } + }); + }); + return fileList; +} -function prendendToTargetOptionFile( - projectName: string, builderName: string, optionName: string, str: string) { +function prependToTargetFiles( + project: workspaces.ProjectDefinition, builderName: Builders, optionName: string, str: string) { return (host: Tree) => { - // Get all known polyfills for browser builders on this project. - const optionValues = getAllOptionValues(host, projectName, builderName, optionName); + const fileList = getFileListForRelevantTargetDefinitions(project, builderName, optionName); - optionValues.forEach(path => { + fileList.forEach((path: string): void => { const data = host.read(path); if (!data) { // If the file doesn't exist, just ignore it. @@ -79,12 +96,16 @@ function prendendToTargetOptionFile( } export default function(options: Schema): Rule { - return (host: Tree) => { - options.name = options.name || getWorkspace(host).defaultProject; + return async (host: Tree) => { if (!options.name) { - throw new Error('Please specify a project using "--name project-name"'); + throw new SchematicsException('Option "name" is required.'); + } + + const workspace = await getWorkspace(host); + const project: workspaces.ProjectDefinition|undefined = workspace.projects.get(options.name); + if (!project) { + throw new SchematicsException(`Invalid project name (${options.name})`); } - validateProjectName(options.name); const localizeStr = `/*************************************************************************************************** @@ -94,8 +115,8 @@ ${localizePolyfill} `; return chain([ - prendendToTargetOptionFile(options.name, Builders.Browser, 'polyfills', localizeStr), - prendendToTargetOptionFile(options.name, Builders.Server, 'main', localizeStr), + prependToTargetFiles(project, Builders.Browser, 'polyfills', localizeStr), + prependToTargetFiles(project, Builders.Server, 'main', localizeStr), ]); }; } diff --git a/packages/localize/schematics/ng-add/index_spec.ts b/packages/localize/schematics/ng-add/index_spec.ts index 9d4cb6fedf..7a69add8f7 100644 --- a/packages/localize/schematics/ng-add/index_spec.ts +++ b/packages/localize/schematics/ng-add/index_spec.ts @@ -42,6 +42,7 @@ export { renderModule, renderModuleFactory } from '@angular/platform-server';`; host.create('src/unrelated-main.server.ts', mainServerContent); host.create('src/another-unrelated-main.server.ts', mainServerContent); host.create('angular.json', JSON.stringify({ + version: 1, projects: { 'demo': { architect: { @@ -155,6 +156,7 @@ export { renderModule, renderModuleFactory } from '@angular/platform-server';`; it('should not break when there are no polyfills', async () => { host.overwrite('angular.json', JSON.stringify({ + version: 1, projects: { 'demo': { architect: {},