From e244c457076d253f303e66c2af9cda20ed6219c4 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:47:43 -0400 Subject: [PATCH 1/2] feat(@angular/build): support custom resolution conditions with applications When using the application build system, a new `conditions` option is now available that allows adding custom package resolution conditions that can adjust the resolution for conditional exports and imports. By default the `module` and `production`/`development` conditions will be present with the later dependent on the `optimization` option. If any custom conditions value is present including an empty array, none of these defaults will be present and must be manually included if needed. The following special conditions will always be present if their respective requirements are satisfied: * es2015 (required by rxjs) * es2020 (APF backwards compatibility) * default * import * require * node * browser For additional information regarding conditional exports/imports: https://nodejs.org/api/packages.html#conditional-exports https://nodejs.org/api/packages.html#subpath-imports --- goldens/public-api/angular/build/index.api.md | 1 + .../build/src/builders/application/options.ts | 1 + .../src/builders/application/schema.json | 7 ++++++ .../tools/esbuild/application-code-bundle.ts | 24 ++++++++++++++----- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index 3e0a2578ed12..88cf484aebd5 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -28,6 +28,7 @@ export type ApplicationBuilderOptions = { browser: string; budgets?: Budget[]; clearScreen?: boolean; + conditions?: string[]; crossOrigin?: CrossOrigin; define?: { [key: string]: string; diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index 2fd1a2f5430c..54bc180df836 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -496,6 +496,7 @@ export async function normalizeOptions( security, templateUpdates: !!options.templateUpdates, incrementalResults: !!options.incrementalResults, + customConditions: options.conditions, }; } diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json index 38232fe0ccbb..670a7a866647 100644 --- a/packages/angular/build/src/builders/application/schema.json +++ b/packages/angular/build/src/builders/application/schema.json @@ -293,6 +293,13 @@ "type": "string" } }, + "conditions": { + "description": "Custom package resolution conditions used to resolve conditional exports/imports. Defaults to ['module', 'development'/'production']. The following special conditions are always present if the requirements are satisfied: 'default', 'import', 'require', 'browser', 'node'.", + "type": "array", + "items": { + "type": "string" + } + }, "fileReplacements": { "description": "Replace compilation source files with other compilation source files in the build.", "type": "array", diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index b955ff1ebfec..7ff6a93a8382 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -546,6 +546,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu loaderExtensions, jsonLogs, i18nOptions, + customConditions, } = options; // Ensure unique hashes for i18n translation changes when using post-process inlining. @@ -563,18 +564,29 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu footer = { js: `/**i18n:${createHash('sha256').update(i18nHash).digest('hex')}*/` }; } + // Core conditions that are always included + const conditions = [ + // Required to support rxjs 7.x which will use es5 code if this condition is not present + 'es2015', + 'es2020', + ]; + + // Append custom conditions if present + if (customConditions) { + conditions.push(...customConditions); + } else { + // Include default conditions + conditions.push('module'); + conditions.push(optimizationOptions.scripts ? 'production' : 'development'); + } + return { absWorkingDir: workspaceRoot, format: 'esm', bundle: true, packages: 'bundle', assetNames: outputNames.media, - conditions: [ - 'es2020', - 'es2015', - 'module', - optimizationOptions.scripts ? 'production' : 'development', - ], + conditions, resolveExtensions: ['.ts', '.tsx', '.mjs', '.js', '.cjs'], metafile: true, legalComments: options.extractLicenses ? 'none' : 'eof', From 8cc398776b1dbecf2ded217727bedfe2cfbe043a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:48:21 -0400 Subject: [PATCH 2/2] test(@angular/build): use correct builder names in integration tests The integration test setup for the dev-server was incorrectly using the name of the `@angular-devkit/build-angular` builders. While this previously had no effect, recent changes have altered the behavior of the schema validation for the `@angular/build` builders. To ensure accurate testing, the names are now correctly specified in the test setup. --- .../tests/options/conditions_spec.ts | 162 ++++++++++++++++++ .../src/builders/dev-server/tests/setup.ts | 4 +- 2 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 packages/angular/build/src/builders/application/tests/options/conditions_spec.ts diff --git a/packages/angular/build/src/builders/application/tests/options/conditions_spec.ts b/packages/angular/build/src/builders/application/tests/options/conditions_spec.ts new file mode 100644 index 000000000000..11e2cdb62ab0 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/options/conditions_spec.ts @@ -0,0 +1,162 @@ +/** + * @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.dev/license + */ + +import { + setupConditionImport, + setTargetMapping, +} from '../../../../../../../../modules/testing/builder/src/dev_prod_mode'; +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Option: "conditions"', () => { + beforeEach(async () => { + await setupConditionImport(harness); + }); + + interface ImportsTestCase { + name: string; + mapping: unknown; + output?: string; + customConditions?: string[]; + } + + const GOOD_TARGET = './src/good.js'; + const BAD_TARGET = './src/bad.js'; + + const emptyArrayCases: ImportsTestCase[] = [ + { + name: 'default fallback without matching condition', + mapping: { + 'never': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + { + name: 'development condition', + mapping: { + 'development': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + { + name: 'production condition', + mapping: { + 'production': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + { + name: 'browser condition (in browser)', + mapping: { + 'browser': GOOD_TARGET, + 'default': BAD_TARGET, + }, + }, + { + name: 'browser condition (in server)', + output: 'server/main.server.mjs', + mapping: { + 'browser': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + ]; + + for (const testCase of emptyArrayCases) { + describe('with empty array ' + testCase.name, () => { + beforeEach(async () => { + await setTargetMapping(harness, testCase.mapping); + }); + + it('resolves to expected target', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + ssr: true, + server: 'src/main.ts', + conditions: [], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + const outputFile = `dist/${testCase.output ?? 'browser/main.js'}`; + harness.expectFile(outputFile).content.toContain('"good-value"'); + harness.expectFile(outputFile).content.not.toContain('"bad-value"'); + }); + }); + } + + const customCases: ImportsTestCase[] = [ + { + name: 'default fallback without matching condition', + mapping: { + 'never': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + { + name: 'development condition', + mapping: { + 'development': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + { + name: 'staging condition', + mapping: { + 'staging': GOOD_TARGET, + 'production': BAD_TARGET, + 'default': BAD_TARGET, + }, + }, + { + name: 'browser condition (in browser)', + mapping: { + 'browser': GOOD_TARGET, + 'staging': BAD_TARGET, + 'default': BAD_TARGET, + }, + }, + { + name: 'browser condition (in server)', + output: 'server/main.server.mjs', + mapping: { + 'browser': BAD_TARGET, + 'default': GOOD_TARGET, + }, + }, + ]; + + for (const testCase of customCases) { + describe('with custom condition ' + testCase.name, () => { + beforeEach(async () => { + await setTargetMapping(harness, testCase.mapping); + }); + + it('resolves to expected target', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + ssr: true, + server: 'src/main.ts', + conditions: ['staging'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + const outputFile = `dist/${testCase.output ?? 'browser/main.js'}`; + harness.expectFile(outputFile).content.toContain('"good-value"'); + harness.expectFile(outputFile).content.not.toContain('"bad-value"'); + }); + }); + } + }); +}); diff --git a/packages/angular/build/src/builders/dev-server/tests/setup.ts b/packages/angular/build/src/builders/dev-server/tests/setup.ts index da9362134b75..2c5906e9644d 100644 --- a/packages/angular/build/src/builders/dev-server/tests/setup.ts +++ b/packages/angular/build/src/builders/dev-server/tests/setup.ts @@ -22,7 +22,7 @@ export * from '../../../../../../../modules/testing/builder/src'; // TODO: Remove and use import after Vite-based dev server is moved to new package export const APPLICATION_BUILDER_INFO = Object.freeze({ - name: '@angular-devkit/build-angular:application', + name: '@angular/build:application', schemaPath: path.join( path.dirname(require.resolve('@angular/build/package.json')), 'src/builders/application/schema.json', @@ -49,7 +49,7 @@ export const APPLICATION_BASE_OPTIONS = Object.freeze({ }); export const DEV_SERVER_BUILDER_INFO = Object.freeze({ - name: '@angular-devkit/build-angular:dev-server', + name: '@angular/build:dev-server', schemaPath: __dirname + '/../schema.json', });