diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 1d80962bcc794..9704c08080f93 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -6706,6 +6706,14 @@ "isExternal": false, "disableCollapsible": false }, + { + "id": "extract-i18n", + "path": "/nx-api/angular/executors/extract-i18n", + "name": "extract-i18n", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, { "id": "webpack-browser", "path": "/nx-api/angular/executors/webpack-browser", diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index 18f1ebd80f917..f31c3b3211c52 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -85,6 +85,15 @@ "path": "/nx-api/angular/executors/application", "type": "executor" }, + "/nx-api/angular/executors/extract-i18n": { + "description": "Extracts i18n messages from source code.", + "file": "generated/packages/angular/executors/extract-i18n.json", + "hidden": false, + "name": "extract-i18n", + "originalFilePath": "/packages/angular/src/executors/extract-i18n/schema.json", + "path": "/nx-api/angular/executors/extract-i18n", + "type": "executor" + }, "/nx-api/angular/executors/webpack-browser": { "description": "The `webpack-browser` executor is very similar to the standard `browser` builder provided by the Angular Devkit. It allows you to build your Angular application to a build artifact that can be hosted online. There are some key differences: \n- Supports Custom Webpack Configurations \n- Supports Incremental Building", "file": "generated/packages/angular/executors/webpack-browser.json", diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 10cd50f108d6f..dfb82d46f5248 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -80,6 +80,15 @@ "path": "angular/executors/application", "type": "executor" }, + { + "description": "Extracts i18n messages from source code.", + "file": "generated/packages/angular/executors/extract-i18n.json", + "hidden": false, + "name": "extract-i18n", + "originalFilePath": "/packages/angular/src/executors/extract-i18n/schema.json", + "path": "angular/executors/extract-i18n", + "type": "executor" + }, { "description": "The `webpack-browser` executor is very similar to the standard `browser` builder provided by the Angular Devkit. It allows you to build your Angular application to a build artifact that can be hosted online. There are some key differences: \n- Supports Custom Webpack Configurations \n- Supports Incremental Building", "file": "generated/packages/angular/executors/webpack-browser.json", diff --git a/docs/generated/packages/angular/executors/extract-i18n.json b/docs/generated/packages/angular/executors/extract-i18n.json new file mode 100644 index 0000000000000..3a607bbbfd126 --- /dev/null +++ b/docs/generated/packages/angular/executors/extract-i18n.json @@ -0,0 +1,55 @@ +{ + "name": "extract-i18n", + "implementation": "/packages/angular/src/executors/extract-i18n/extract-i18n.impl.ts", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Schema for Nx extract-i18n Executor", + "description": "Extracts i18n messages from source code.", + "outputCapture": "direct-nodejs", + "type": "object", + "properties": { + "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": { + "type": "string", + "description": "Output format for the generated file.", + "default": "xlf", + "enum": [ + "xmb", + "xlf", + "xlif", + "xliff", + "xlf2", + "xliff2", + "json", + "arb", + "legacy-migrate" + ] + }, + "progress": { + "type": "boolean", + "description": "Log progress to the console.", + "default": true + }, + "outputPath": { + "type": "string", + "description": "Path where output will be placed." + }, + "outFile": { + "type": "string", + "description": "Name of the file to output." + } + }, + "additionalProperties": false, + "required": ["buildTarget"], + "presets": [] + }, + "description": "Extracts i18n messages from source code.", + "aliases": [], + "hidden": false, + "path": "/packages/angular/src/executors/extract-i18n/schema.json", + "type": "executor" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 80e03cb48da40..d948ddb67c8cd 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -325,6 +325,7 @@ - [browser-esbuild](/nx-api/angular/executors/browser-esbuild) - [module-federation-dev-server](/nx-api/angular/executors/module-federation-dev-server) - [application](/nx-api/angular/executors/application) + - [extract-i18n](/nx-api/angular/executors/extract-i18n) - [webpack-browser](/nx-api/angular/executors/webpack-browser) - [dev-server](/nx-api/angular/executors/dev-server) - [webpack-server](/nx-api/angular/executors/webpack-server) diff --git a/packages/angular/executors.json b/packages/angular/executors.json index 31c610a31c197..592c8960db329 100644 --- a/packages/angular/executors.json +++ b/packages/angular/executors.json @@ -29,6 +29,11 @@ "implementation": "./src/executors/application/application.impl", "schema": "./src/executors/application/schema.json", "description": "Builds an application with esbuild with support for incremental builds. _Note: this is only supported in Angular versions >= 17.0.0_." + }, + "extract-i18n": { + "implementation": "./src/executors/extract-i18n/extract-i18n.impl", + "schema": "./src/executors/extract-i18n/schema.json", + "description": "Extracts i18n messages from source code." } }, "builders": { diff --git a/packages/angular/executors.ts b/packages/angular/executors.ts index 8149bc6e1f68d..e874aadc48bc1 100644 --- a/packages/angular/executors.ts +++ b/packages/angular/executors.ts @@ -7,6 +7,7 @@ export * from './src/executors/ng-packagr-lite/ng-packagr-lite.impl'; export * from './src/executors/package/package.impl'; export * from './src/executors/browser-esbuild/browser-esbuild.impl'; export * from './src/executors/application/application.impl'; +export * from './src/executors/extract-i18n/extract-i18n.impl'; import { executeDevServerBuilder } from './src/builders/dev-server/dev-server.impl'; diff --git a/packages/angular/src/builders/dev-server/dev-server.impl.ts b/packages/angular/src/builders/dev-server/dev-server.impl.ts index 0c52b48a9d134..d58faac7f55aa 100644 --- a/packages/angular/src/builders/dev-server/dev-server.impl.ts +++ b/packages/angular/src/builders/dev-server/dev-server.impl.ts @@ -1,16 +1,10 @@ import type { BuilderContext } from '@angular-devkit/architect'; -import type { - ApplicationBuilderOptions, - BrowserBuilderOptions, - DevServerBuilderOptions, -} from '@angular-devkit/build-angular'; -import type { Schema as BrowserEsbuildBuilderOptions } from '@angular-devkit/build-angular/src/builders/browser-esbuild/schema'; +import type { DevServerBuilderOptions } from '@angular-devkit/build-angular'; import { joinPathFragments, normalizePath, parseTargetString, readCachedProjectGraph, - type Target, } from '@nx/devkit'; import { getRootTsConfigPath } from '@nx/js'; import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils'; @@ -28,6 +22,7 @@ import { loadPlugins, type PluginSpec, } from '../../executors/utilities/esbuild-extensions'; +import { patchBuilderContext } from '../../executors/utilities/patch-builder-context'; import { createTmpTsConfigForBuildableLibs } from '../utilities/buildable-libs'; import { mergeCustomWebpackConfig, @@ -288,60 +283,3 @@ async function loadIndexHtmlFileTransformer( ) : await loadIndexHtmlTransformer(pathToIndexFileTransformer, tsConfig); } - -const executorToBuilderMap = new Map([ - [ - '@nx/angular:browser-esbuild', - '@angular-devkit/build-angular:browser-esbuild', - ], - ['@nx/angular:application', '@angular-devkit/build-angular:application'], -]); - -function patchBuilderContext( - context: BuilderContext, - isUsingEsbuildBuilder: boolean, - buildTarget: Target -): void { - const originalGetBuilderNameForTarget = context.getBuilderNameForTarget; - context.getBuilderNameForTarget = async (target) => { - const builderName = await originalGetBuilderNameForTarget(target); - - if (executorToBuilderMap.has(builderName)) { - return executorToBuilderMap.get(builderName)!; - } - - return builderName; - }; - - if (isUsingEsbuildBuilder) { - const originalGetTargetOptions = context.getTargetOptions; - context.getTargetOptions = async (target) => { - const options = await originalGetTargetOptions(target); - - if ( - target.project === buildTarget.project && - target.target === buildTarget.target && - target.configuration === buildTarget.configuration - ) { - cleanBuildTargetOptions(options); - } - - return options; - }; - } -} - -function cleanBuildTargetOptions( - options: any -): - | ApplicationBuilderOptions - | BrowserBuilderOptions - | BrowserEsbuildBuilderOptions { - delete options.buildLibsFromSource; - delete options.customWebpackConfig; - delete options.indexHtmlTransformer; - delete options.indexFileTransformer; - delete options.plugins; - - return options; -} diff --git a/packages/angular/src/executors/extract-i18n/extract-i18n.impl.ts b/packages/angular/src/executors/extract-i18n/extract-i18n.impl.ts new file mode 100644 index 0000000000000..188587df20330 --- /dev/null +++ b/packages/angular/src/executors/extract-i18n/extract-i18n.impl.ts @@ -0,0 +1,68 @@ +import type { ExtractI18nBuilderOptions } from '@angular-devkit/build-angular'; +import { parseTargetString, type ExecutorContext } from '@nx/devkit'; +import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter'; +import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph'; +import { getInstalledAngularVersionInfo } from '../utilities/angular-version-utils'; +import { patchBuilderContext } from '../utilities/patch-builder-context'; +import type { ExtractI18nExecutorOptions } from './schema'; + +export default async function* extractI18nExecutor( + options: ExtractI18nExecutorOptions, + context: ExecutorContext +) { + const parsedBuildTarget = parseTargetString(options.buildTarget, context); + const browserTargetProjectConfiguration = readCachedProjectConfiguration( + parsedBuildTarget.project + ); + + const buildTarget = + browserTargetProjectConfiguration.targets[parsedBuildTarget.target]; + + const isUsingEsbuildBuilder = [ + '@angular-devkit/build-angular:application', + '@angular-devkit/build-angular:browser-esbuild', + '@nx/angular:application', + '@nx/angular:browser-esbuild', + ].includes(buildTarget.executor); + + const builderContext = await createBuilderContext( + { + builderName: 'extrct-i18n', + description: 'Extracts i18n messages from source code.', + optionSchema: await import('./schema.json'), + }, + context + ); + + /** + * The Angular CLI extract-i18n builder make some decisions based on the build + * target builder but it only considers `@angular-devkit/build-angular:*` + * builders. Since we are using a custom builder, we patch the context to + * handle `@nx/angular:*` executors. + */ + patchBuilderContext(builderContext, isUsingEsbuildBuilder, parsedBuildTarget); + + const { executeExtractI18nBuilder } = await import( + '@angular-devkit/build-angular' + ); + const delegateBuilderOptions = getDelegateBuilderOptions(options); + + return await executeExtractI18nBuilder( + delegateBuilderOptions, + builderContext + ); +} + +function getDelegateBuilderOptions( + options: ExtractI18nExecutorOptions +): ExtractI18nBuilderOptions { + const delegateBuilderOptions: ExtractI18nBuilderOptions = { ...options }; + + const { major: angularMajorVersion } = getInstalledAngularVersionInfo(); + if (angularMajorVersion <= 17) { + delegateBuilderOptions.browserTarget = delegateBuilderOptions.buildTarget; + delete delegateBuilderOptions.buildTarget; + } + + return delegateBuilderOptions; +} diff --git a/packages/angular/src/executors/extract-i18n/schema.d.ts b/packages/angular/src/executors/extract-i18n/schema.d.ts new file mode 100644 index 0000000000000..5b8e2d4760d0b --- /dev/null +++ b/packages/angular/src/executors/extract-i18n/schema.d.ts @@ -0,0 +1,8 @@ +import type { ExtractI18nBuilderOptions } from '@angular-devkit/build-angular'; + +export type ExtractI18nExecutorOptions = Omit< + ExtractI18nBuilderOptions, + 'browserTarget' +> & { + buildTarget: string; +}; diff --git a/packages/angular/src/executors/extract-i18n/schema.json b/packages/angular/src/executors/extract-i18n/schema.json new file mode 100644 index 0000000000000..79dfffdc3e0f6 --- /dev/null +++ b/packages/angular/src/executors/extract-i18n/schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Schema for Nx extract-i18n Executor", + "description": "Extracts i18n messages from source code.", + "outputCapture": "direct-nodejs", + "type": "object", + "properties": { + "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": { + "type": "string", + "description": "Output format for the generated file.", + "default": "xlf", + "enum": [ + "xmb", + "xlf", + "xlif", + "xliff", + "xlf2", + "xliff2", + "json", + "arb", + "legacy-migrate" + ] + }, + "progress": { + "type": "boolean", + "description": "Log progress to the console.", + "default": true + }, + "outputPath": { + "type": "string", + "description": "Path where output will be placed." + }, + "outFile": { + "type": "string", + "description": "Name of the file to output." + } + }, + "additionalProperties": false, + "required": ["buildTarget"] +} diff --git a/packages/angular/src/executors/utilities/patch-builder-context.ts b/packages/angular/src/executors/utilities/patch-builder-context.ts new file mode 100644 index 0000000000000..071a74bb1fa70 --- /dev/null +++ b/packages/angular/src/executors/utilities/patch-builder-context.ts @@ -0,0 +1,64 @@ +import type { BuilderContext } from '@angular-devkit/architect'; +import type { + ApplicationBuilderOptions, + BrowserBuilderOptions, +} from '@angular-devkit/build-angular'; +import type { Schema as BrowserEsbuildBuilderOptions } from '@angular-devkit/build-angular/src/builders/browser-esbuild/schema'; +import type { Target } from '@nx/devkit'; + +const executorToBuilderMap = new Map([ + [ + '@nx/angular:browser-esbuild', + '@angular-devkit/build-angular:browser-esbuild', + ], + ['@nx/angular:application', '@angular-devkit/build-angular:application'], +]); + +export function patchBuilderContext( + context: BuilderContext, + isUsingEsbuildBuilder: boolean, + buildTarget: Target +): void { + const originalGetBuilderNameForTarget = context.getBuilderNameForTarget; + context.getBuilderNameForTarget = async (target) => { + const builderName = await originalGetBuilderNameForTarget(target); + + if (executorToBuilderMap.has(builderName)) { + return executorToBuilderMap.get(builderName)!; + } + + return builderName; + }; + + if (isUsingEsbuildBuilder) { + const originalGetTargetOptions = context.getTargetOptions; + context.getTargetOptions = async (target) => { + const options = await originalGetTargetOptions(target); + + if ( + target.project === buildTarget.project && + target.target === buildTarget.target && + target.configuration === buildTarget.configuration + ) { + cleanBuildTargetOptions(options); + } + + return options; + }; + } +} + +function cleanBuildTargetOptions( + options: any +): + | ApplicationBuilderOptions + | BrowserBuilderOptions + | BrowserEsbuildBuilderOptions { + delete options.buildLibsFromSource; + delete options.customWebpackConfig; + delete options.indexHtmlTransformer; + delete options.indexFileTransformer; + delete options.plugins; + + return options; +}