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/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', }); 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',