From 7f57123fd40b345d7880cb9e2eccd4757c0fb6ee Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 30 Nov 2023 19:46:19 -0500 Subject: [PATCH] feat(@angular-devkit/build-angular): add define build option to application builder The `application` builder now supports a new option named `define`. This option allows global identifiers present in the code to be replaced with another value at build time. This is similar to the behavior of Webpack's `DefinePlugin` which was previously used with some custom Webpack configurations that used third-party builders. The option has similar capabilities to the `esbuild` option of the same name. The documentation for that option can be found here: https://esbuild.github.io/api/#define The command line capabilities of the Angular CLI option are not yet implemented and will added in a future change. The option within the `angular.json` configuration file is of the form of an object. The keys of the object represent the global identifier to replace and the values of the object represent the corresponding replacement value for the identifier. An example is as follows: ``` "define": { "SOME_CONSTANT": "5", "ANOTHER": "'this is a string literal'" } ``` All replacement values are defined as strings within the configuration file. If the replacement is intended to be an actual string literal, it should be enclosed in single quote marks. This allows the flexibility of using any valid JSON type as well as a different identifier as a replacement. Additionally, TypeScript needs to be aware of the module type for the import to prevent type-checking errors during the build. This can be accomplished with an additional type definition file within the application source code (`src/types.d.ts`, for example) with the following or similar content: ``` declare const SOME_CONSTANT: number; declare const ANOTHER: string; ``` The default project configuration is already setup to use any type definition files present in the project source directories. If the TypeScript configuration for the project has been altered, the tsconfig may need to be adjusted to reference this newly added type definition file. An important caveat to the option is that it does not function when used with values contained within Angular metadata such as a Component or Directive decorator. This limitation was present with previous third-party builder usage as well. --- .../angular_devkit/build_angular/index.md | 3 + .../src/builders/application/options.ts | 2 + .../src/builders/application/schema.json | 7 ++ .../application/tests/options/define_spec.ts | 65 +++++++++++++++++++ .../tools/esbuild/application-code-bundle.ts | 1 + 5 files changed, 78 insertions(+) create mode 100644 packages/angular_devkit/build_angular/src/builders/application/tests/options/define_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 0085f55f7093..f69fe3eae25f 100644 --- a/goldens/public-api/angular_devkit/build_angular/index.md +++ b/goldens/public-api/angular_devkit/build_angular/index.md @@ -32,6 +32,9 @@ export interface ApplicationBuilderOptions { budgets?: Budget_2[]; clearScreen?: boolean; crossOrigin?: CrossOrigin_2; + define?: { + [key: string]: string; + }; deleteOutputPath?: boolean; externalDependencies?: string[]; extractLicenses?: boolean; diff --git a/packages/angular_devkit/build_angular/src/builders/application/options.ts b/packages/angular_devkit/build_angular/src/builders/application/options.ts index cb2aa74b5873..8eb6861fe4f2 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/options.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/options.ts @@ -295,6 +295,7 @@ export async function normalizeOptions( budgets, deployUrl, clearScreen, + define, } = options; // Return all the normalized options @@ -350,6 +351,7 @@ export async function normalizeOptions( jsonLogs: useJSONBuildLogs, colors: colors.enabled, clearScreen, + define, }; } diff --git a/packages/angular_devkit/build_angular/src/builders/application/schema.json b/packages/angular_devkit/build_angular/src/builders/application/schema.json index 1aff297e63b2..4cd4359bdc62 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/application/schema.json @@ -216,6 +216,13 @@ "^\\.\\S+$": { "enum": ["text", "binary", "file", "empty"] } } }, + "define": { + "description": "Defines global identifiers that will be replaced with a specified constant value when found in any JavaScript or TypeScript code including libraries. The value will be used directly. String values must be put in quotes. Identifiers within Angular metadata such as Component Decorators will not be replaced.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "fileReplacements": { "description": "Replace compilation source files with other compilation source files in the build.", "type": "array", diff --git a/packages/angular_devkit/build_angular/src/builders/application/tests/options/define_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/define_spec.ts new file mode 100644 index 000000000000..3c6c8897d5eb --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/define_spec.ts @@ -0,0 +1,65 @@ +/** + * @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 { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Option: "define"', () => { + it('should replace a value in application code when specified as a number', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + define: { + 'AN_INTEGER': '42', + }, + }); + + await harness.writeFile('./src/types.d.ts', 'declare const AN_INTEGER: number;'); + await harness.writeFile('src/main.ts', 'console.log(AN_INTEGER);'); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.not.toContain('AN_INTEGER'); + harness.expectFile('dist/browser/main.js').content.toContain('(42)'); + }); + + it('should replace a value in application code when specified as a string', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + define: { + 'A_STRING': '"42"', + }, + }); + + await harness.writeFile('./src/types.d.ts', 'declare const A_STRING: string;'); + await harness.writeFile('src/main.ts', 'console.log(A_STRING);'); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.not.toContain('A_STRING'); + harness.expectFile('dist/browser/main.js').content.toContain('("42")'); + }); + + it('should replace a value in application code when specified as a boolean', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + define: { + 'A_BOOLEAN': 'true', + }, + }); + + await harness.writeFile('./src/types.d.ts', 'declare const A_BOOLEAN: boolean;'); + await harness.writeFile('src/main.ts', 'console.log(A_BOOLEAN);'); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/main.js').content.not.toContain('A_BOOLEAN'); + harness.expectFile('dist/browser/main.js').content.toContain('(true)'); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts index 1ddfdd592658..57c01b3f0db5 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts @@ -379,6 +379,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu write: false, preserveSymlinks, define: { + ...options.define, // Only set to false when script optimizations are enabled. It should not be set to true because // Angular turns `ngDevMode` into an object for development debugging purposes when not defined // which a constant true value would break.