From f28f0fa84b5d2765573e33c5943191b436bde67e Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 10 Oct 2023 15:37:20 +0000 Subject: [PATCH] feat(@angular-devkit/build-angular): add `buildTarget` option to dev-server and `extract-i18n` builders This is to better match the nature of the application builder where the target can be both browser and server. DEPRECATED: The `browserTarget` in the dev-server and extract-i18n builders have been deprecated in favor of `buildTarget`. --- .../angular_devkit/build_angular/index.md | 8 +- .../behavior/typescript-rebuild-lazy_spec.ts | 2 +- .../src/builders/dev-server/builder.ts | 2 +- .../src/builders/dev-server/options.ts | 5 +- .../src/builders/dev-server/schema.json | 8 +- .../src/builders/dev-server/tests/setup.ts | 2 +- .../src/builders/dev-server/vite-server.ts | 2 +- .../src/builders/dev-server/webpack-server.ts | 2 +- .../extract-i18n/application-extraction.ts | 4 +- .../src/builders/extract-i18n/builder.ts | 7 +- .../src/builders/extract-i18n/options.ts | 5 +- .../src/builders/extract-i18n/schema.json | 8 +- .../extract-i18n/webpack-extraction.ts | 2 +- .../build_angular/src/utils/build-options.ts | 2 +- .../schematics/angular/application/index.ts | 6 +- .../migrations/migration-collection.json | 5 + .../update-17/update-workspace-config.ts | 32 +++++ .../update-17/update-workspace-config_spec.ts | 112 ++++++++++++++++++ 18 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 packages/schematics/angular/migrations/update-17/update-workspace-config.ts create mode 100644 packages/schematics/angular/migrations/update-17/update-workspace-config_spec.ts diff --git a/goldens/public-api/angular_devkit/build_angular/index.md b/goldens/public-api/angular_devkit/build_angular/index.md index 328f9814803b..00108df05faf 100644 --- a/goldens/public-api/angular_devkit/build_angular/index.md +++ b/goldens/public-api/angular_devkit/build_angular/index.md @@ -107,7 +107,9 @@ export enum CrossOrigin { // @public export interface DevServerBuilderOptions { allowedHosts?: string[]; - browserTarget: string; + // @deprecated + browserTarget?: string; + buildTarget?: string; disableHostCheck?: boolean; forceEsbuild?: boolean; headers?: { @@ -176,7 +178,9 @@ export type ExecutionTransformer = (input: T) => T | Promise; // @public export interface ExtractI18nBuilderOptions { - browserTarget: string; + // @deprecated + browserTarget?: string; + buildTarget?: string; format?: Format; outFile?: string; outputPath?: string; diff --git a/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts index 5bd14469321d..3f328e326e3c 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts @@ -44,7 +44,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const builderAbort = new AbortController(); const buildCount = await firstValueFrom( - harness.execute({ outputLogsOnFailure: true, signal: builderAbort.signal }).pipe( + harness.execute({ outputLogsOnFailure: false, signal: builderAbort.signal }).pipe( timeout(20_000), concatMap(async ({ result, logs }, index) => { switch (index) { diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts index cb68e29afce9..5af0eca63017 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts @@ -74,7 +74,7 @@ async function initialize( await purgeStaleBuildCache(context); const normalizedOptions = await normalizeOptions(context, projectName, initialOptions); - const builderName = await context.getBuilderNameForTarget(normalizedOptions.browserTarget); + const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget); if ( !normalizedOptions.disableHostCheck && diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/options.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/options.ts index 55bb233ca2a8..2102c9284a4e 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/options.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/options.ts @@ -34,7 +34,8 @@ export async function normalizeOptions( const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot); - const browserTarget = targetFromTargetString(options.browserTarget); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const buildTarget = targetFromTargetString(options.buildTarget ?? options.browserTarget!); // Initial options to keep const { @@ -60,7 +61,7 @@ export async function normalizeOptions( // Return all the normalized options return { - browserTarget, + buildTarget, host: host ?? 'localhost', port: port ?? 4200, poll, diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json b/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json index 28760e6e2587..93ce53407a27 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json @@ -7,6 +7,12 @@ "browserTarget": { "type": "string", "description": "A browser builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", + "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$", + "x-deprecated": "Use 'buildTarget' instead." + }, + "buildTarget": { + "type": "string", + "description": "A build builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" }, "port": { @@ -103,5 +109,5 @@ } }, "additionalProperties": false, - "required": ["browserTarget"] + "anyOf": [{ "required": ["buildTarget"] }, { "required": ["browserTarget"] }] } diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/setup.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/setup.ts index 6ee052567874..2fd25241330f 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/setup.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/setup.ts @@ -30,7 +30,7 @@ export const DEV_SERVER_BUILDER_INFO = Object.freeze({ * supports parallel test execution. */ export const BASE_OPTIONS = Object.freeze({ - browserTarget: 'test:build', + buildTarget: 'test:build', port: 0, }); diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts index 97e413c18202..c1df69d936fd 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts @@ -51,7 +51,7 @@ export async function* serveWithVite( ): AsyncIterableIterator { // Get the browser configuration from the target name. const rawBrowserOptions = (await context.getTargetOptions( - serverOptions.browserTarget, + serverOptions.buildTarget, )) as json.JsonObject & BrowserBuilderOptions; const browserOptions = (await context.validateOptions( diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/webpack-server.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/webpack-server.ts index c67f2626d40e..7ba2ea697175 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/webpack-server.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/webpack-server.ts @@ -86,7 +86,7 @@ export function serveWebpackBrowser( // Get the browser configuration from the target name. const rawBrowserOptions = (await context.getTargetOptions( - options.browserTarget, + options.buildTarget, )) as json.JsonObject & BrowserBuilderSchema; if (rawBrowserOptions.outputHashing && rawBrowserOptions.outputHashing !== OutputHashing.None) { diff --git a/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts b/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts index 699641f107b4..6fee28118c3e 100644 --- a/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts +++ b/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts @@ -29,9 +29,9 @@ export async function extractMessages( }> { const messages: LocalizeMessage[] = []; - // Setup the build options for the application based on the browserTarget option + // Setup the build options for the application based on the buildTarget option const buildOptions = (await context.validateOptions( - await context.getTargetOptions(options.browserTarget), + await context.getTargetOptions(options.buildTarget), builderName, )) as unknown as ApplicationBuilderInternalOptions; buildOptions.optimization = false; diff --git a/packages/angular_devkit/build_angular/src/builders/extract-i18n/builder.ts b/packages/angular_devkit/build_angular/src/builders/extract-i18n/builder.ts index fa34a8b48653..86427b0756f6 100644 --- a/packages/angular_devkit/build_angular/src/builders/extract-i18n/builder.ts +++ b/packages/angular_devkit/build_angular/src/builders/extract-i18n/builder.ts @@ -43,9 +43,8 @@ export async function execute( // The package is a peer dependency and might not be present let localizeToolsModule; try { - localizeToolsModule = await loadEsmModule( - '@angular/localize/tools', - ); + localizeToolsModule = + await loadEsmModule('@angular/localize/tools'); } catch { return { success: false, @@ -57,7 +56,7 @@ export async function execute( // Normalize options const normalizedOptions = await normalizeOptions(context, projectName, options); - const builderName = await context.getBuilderNameForTarget(normalizedOptions.browserTarget); + const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget); // Extract messages based on configured builder let extractionResult; diff --git a/packages/angular_devkit/build_angular/src/builders/extract-i18n/options.ts b/packages/angular_devkit/build_angular/src/builders/extract-i18n/options.ts index 1db3e257abb5..b65039ccb775 100644 --- a/packages/angular_devkit/build_angular/src/builders/extract-i18n/options.ts +++ b/packages/angular_devkit/build_angular/src/builders/extract-i18n/options.ts @@ -33,7 +33,8 @@ export async function normalizeOptions( const projectMetadata = await context.getProjectMetadata(projectName); const projectRoot = path.join(workspaceRoot, (projectMetadata.root as string | undefined) ?? ''); - const browserTarget = targetFromTargetString(options.browserTarget); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const buildTarget = targetFromTargetString(options.buildTarget ?? options.browserTarget!); const i18nOptions = createI18nOptions(projectMetadata); @@ -62,7 +63,7 @@ export async function normalizeOptions( return { workspaceRoot, projectRoot, - browserTarget, + buildTarget, i18nOptions, format, outFile, diff --git a/packages/angular_devkit/build_angular/src/builders/extract-i18n/schema.json b/packages/angular_devkit/build_angular/src/builders/extract-i18n/schema.json index f4bf78ba361d..22bb8c36b460 100644 --- a/packages/angular_devkit/build_angular/src/builders/extract-i18n/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/extract-i18n/schema.json @@ -7,6 +7,12 @@ "browserTarget": { "type": "string", "description": "A browser builder target to extract i18n messages in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", + "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$", + "x-deprecated": "Use 'buildTarget' instead." + }, + "buildTarget": { + "type": "string", + "description": "A builder target to extract i18n messages in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" }, "format": { @@ -30,5 +36,5 @@ } }, "additionalProperties": false, - "required": ["browserTarget"] + "anyOf": [{ "required": ["buildTarget"] }, { "required": ["browserTarget"] }] } diff --git a/packages/angular_devkit/build_angular/src/builders/extract-i18n/webpack-extraction.ts b/packages/angular_devkit/build_angular/src/builders/extract-i18n/webpack-extraction.ts index 5691d5f9a8aa..21ea02f295bb 100644 --- a/packages/angular_devkit/build_angular/src/builders/extract-i18n/webpack-extraction.ts +++ b/packages/angular_devkit/build_angular/src/builders/extract-i18n/webpack-extraction.ts @@ -41,7 +41,7 @@ export async function extractMessages( let useLegacyIds = true; const browserOptions = await context.validateOptions( - await context.getTargetOptions(options.browserTarget), + await context.getTargetOptions(options.buildTarget), builderName, ); diff --git a/packages/angular_devkit/build_angular/src/utils/build-options.ts b/packages/angular_devkit/build_angular/src/utils/build-options.ts index 7856f42fe6a2..c24f78868f96 100644 --- a/packages/angular_devkit/build_angular/src/utils/build-options.ts +++ b/packages/angular_devkit/build_angular/src/utils/build-options.ts @@ -76,7 +76,7 @@ export interface BuildOptions { export interface WebpackDevServerOptions extends BuildOptions, - Omit {} + Omit {} export interface WebpackConfigOptions { root: string; diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 8e77db87f25d..b05e9e11dcb0 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -263,17 +263,17 @@ function addAppToWorkspaceFile( options: {}, configurations: { production: { - browserTarget: `${options.name}:build:production`, + buildTarget: `${options.name}:build:production`, }, development: { - browserTarget: `${options.name}:build:development`, + buildTarget: `${options.name}:build:development`, }, }, }, 'extract-i18n': { builder: Builders.ExtractI18n, options: { - browserTarget: `${options.name}:build`, + buildTarget: `${options.name}:build`, }, }, test: options.minimal diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index a354c9b0b682..b7bd5a1a2c84 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -4,6 +4,11 @@ "version": "17.0.0", "factory": "./update-17/replace-nguniversal-builders", "description": "Replace usages of '@nguniversal/builders' with '@angular-devkit/build-angular'." + }, + "update-workspace-config": { + "version": "17.0.0", + "factory": "./update-17/update-workspace-config", + "description": "Replace deprecated options in 'angular.json'." } } } diff --git a/packages/schematics/angular/migrations/update-17/update-workspace-config.ts b/packages/schematics/angular/migrations/update-17/update-workspace-config.ts new file mode 100644 index 000000000000..802c487a5381 --- /dev/null +++ b/packages/schematics/angular/migrations/update-17/update-workspace-config.ts @@ -0,0 +1,32 @@ +/** + * @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 { Rule, chain } from '@angular-devkit/schematics'; +import { removePackageJsonDependency } from '../../utility/dependencies'; +import { allTargetOptions, updateWorkspace } from '../../utility/workspace'; +import { Builders, ProjectType } from '../../utility/workspace-models'; + +export default function (): Rule { + return updateWorkspace((workspace) => { + for (const [, project] of workspace.projects) { + if (project.extensions.projectType !== ProjectType.Application) { + // Only interested in application projects since these changes only effects application builders + continue; + } + + for (const [, target] of project.targets) { + if (target.builder === Builders.ExtractI18n || target.builder === Builders.DevServer) { + for (const [, options] of allTargetOptions(target, false)) { + options['buildTarget'] = options['browserTarget']; + delete options['browserTarget']; + } + } + } + } + }); +} diff --git a/packages/schematics/angular/migrations/update-17/update-workspace-config_spec.ts b/packages/schematics/angular/migrations/update-17/update-workspace-config_spec.ts new file mode 100644 index 000000000000..51067cee49d6 --- /dev/null +++ b/packages/schematics/angular/migrations/update-17/update-workspace-config_spec.ts @@ -0,0 +1,112 @@ +/** + * @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 { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models'; + +function createWorkSpaceConfig(tree: UnitTestTree) { + const angularConfig: WorkspaceSchema = { + version: 1, + projects: { + app: { + root: '/project/lib', + sourceRoot: '/project/app/src', + projectType: ProjectType.Application, + prefix: 'app', + architect: { + 'app-shell': { + builder: Builders.AppShell, + options: { + browserTarget: 'app:build', + serverTarget: 'app:server', + route: '', + }, + configurations: { + production: { + browserTarget: 'app:build:production', + serverTarget: 'app:server:production', + }, + }, + }, + serve: { + builder: Builders.DevServer, + options: { + browserTarget: 'app:build:development', + }, + configurations: { + production: { + browserTarget: 'app:build:production', + }, + }, + }, + i18n: { + builder: Builders.ExtractI18n, + options: { + browserTarget: 'app:build:production', + }, + }, + }, + }, + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); +} + +describe(`Migration to update 'angular.json'.`, () => { + const schematicName = 'update-workspace-config'; + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + createWorkSpaceConfig(tree); + }); + + it(`should replace 'browserTarget' when using '@angular-devkit/build-angular:dev-server'`, async () => { + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + const { browserTarget, buildTarget } = app.architect['serve'].options; + expect(browserTarget).toBeUndefined(); + expect(buildTarget).toBe('app:build:development'); + + const { browserTarget: browserTargetProd, buildTarget: buildTargetProd } = + app.architect['serve'].configurations['production']; + expect(browserTargetProd).toBeUndefined(); + expect(buildTargetProd).toBe('app:build:production'); + }); + + it(`should replace 'browserTarget' when using '@angular-devkit/build-angular:extract-i18n'`, async () => { + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + const { browserTarget, buildTarget } = app.architect['i18n'].options; + expect(browserTarget).toBeUndefined(); + expect(buildTarget).toBe('app:build:production'); + }); + + it(`should not replace 'browserTarget' when using other builders`, async () => { + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + const { browserTarget, buildTarget } = app.architect['app-shell'].options; + expect(browserTarget).toBe('app:build'); + expect(buildTarget).toBeUndefined(); + }); +});