Skip to content

Commit 67bdb23

Browse files
committed
feat(@angular/build): set development/production condition
Ensures that we consistently set "development" for non-optimized and "production" for optimized builds. This is consistent with other bundlers (Vite/webpack/parcel/...).
1 parent dcbdca8 commit 67bdb23

File tree

6 files changed

+229
-3
lines changed

6 files changed

+229
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { BuilderHarness } from './builder-harness';
10+
11+
export const GOOD_TARGET = './src/good.js';
12+
export const BAD_TARGET = './src/bad.js';
13+
14+
/** Setup project for use of conditional imports. */
15+
export async function setupConditionImport(harness: BuilderHarness<unknown>) {
16+
// Files that can be used as targets for the conditional import.
17+
await harness.writeFile('src/good.ts', `export const VALUE = 'good-value';`);
18+
await harness.writeFile('src/bad.ts', `export const VALUE = 'bad-value';`);
19+
20+
// Simple application file that accesses conditional code.
21+
await harness.writeFile(
22+
'src/main.ts',
23+
`import {VALUE} from '#target';
24+
console.log(VALUE);
25+
`,
26+
);
27+
28+
// Ensure that good/bad can be resolved from tsconfig.
29+
const tsconfig = JSON.parse(harness.readFile('src/tsconfig.app.json')) as TypeScriptConfig;
30+
tsconfig.compilerOptions.moduleResolution = 'bundler';
31+
tsconfig.files.push('good.ts', 'bad.ts');
32+
await harness.writeFile('src/tsconfig.app.json', JSON.stringify(tsconfig));
33+
}
34+
35+
/** Update package.json with the given mapping for #target. */
36+
export async function setTargetMapping(harness: BuilderHarness<unknown>, mapping: unknown) {
37+
await harness.writeFile(
38+
'package.json',
39+
JSON.stringify({
40+
name: 'ng-test-app',
41+
imports: {
42+
'#target': mapping,
43+
},
44+
}),
45+
);
46+
}
47+
48+
interface TypeScriptConfig {
49+
compilerOptions: {
50+
moduleResolution: string;
51+
};
52+
files: string[];
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
setupConditionImport,
11+
setTargetMapping,
12+
} from '../../../../../../../../modules/testing/builder/src/dev_prod_mode';
13+
import { buildApplication } from '../../index';
14+
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
15+
16+
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
17+
describe('Behavior: "conditional imports"', () => {
18+
beforeEach(async () => {
19+
await setupConditionImport(harness);
20+
});
21+
22+
interface ImportsTestCase {
23+
name: string;
24+
mapping: unknown;
25+
}
26+
27+
const GOOD_TARGET = './src/good.js';
28+
const BAD_TARGET = './src/bad.js';
29+
30+
const testCases: ImportsTestCase[] = [
31+
{ name: 'simple string', mapping: GOOD_TARGET },
32+
{
33+
name: 'default fallback without matching condition',
34+
mapping: {
35+
'never': BAD_TARGET,
36+
'default': GOOD_TARGET,
37+
},
38+
},
39+
{
40+
name: 'development condition',
41+
mapping: {
42+
'development': BAD_TARGET,
43+
'default': GOOD_TARGET,
44+
},
45+
},
46+
{
47+
name: 'production condition',
48+
mapping: {
49+
'production': GOOD_TARGET,
50+
'default': BAD_TARGET,
51+
},
52+
},
53+
];
54+
55+
for (const testCase of testCases) {
56+
describe(testCase.name, () => {
57+
beforeEach(async () => {
58+
await setTargetMapping(harness, testCase.mapping);
59+
});
60+
61+
it('resolves to expected target', async () => {
62+
harness.useTarget('build', {
63+
...BASE_OPTIONS,
64+
optimization: true,
65+
});
66+
67+
const { result } = await harness.executeOnce();
68+
69+
expect(result?.success).toBeTrue();
70+
harness.expectFile('dist/browser/main.js').content.toContain('"good-value"');
71+
harness.expectFile('dist/browser/main.js').content.not.toContain('"bad-value"');
72+
});
73+
});
74+
}
75+
});
76+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
setupConditionImport,
11+
setTargetMapping,
12+
} from '../../../../../../../../modules/testing/builder/src/dev_prod_mode';
13+
import { executeDevServer } from '../../index';
14+
import { executeOnceAndFetch } from '../execute-fetch';
15+
import { describeServeBuilder } from '../jasmine-helpers';
16+
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
17+
18+
describeServeBuilder(
19+
executeDevServer,
20+
DEV_SERVER_BUILDER_INFO,
21+
(harness, setupTarget, isApplicationBuilder) => {
22+
describe('Behavior: "conditional imports"', () => {
23+
if (!isApplicationBuilder) {
24+
it('requires esbuild', () => {
25+
expect(true).toBeTrue();
26+
});
27+
28+
return;
29+
}
30+
31+
beforeEach(async () => {
32+
setupTarget(harness);
33+
34+
await setupConditionImport(harness);
35+
});
36+
37+
interface ImportsTestCase {
38+
name: string;
39+
mapping: unknown;
40+
}
41+
42+
const GOOD_TARGET = './src/good.js';
43+
const BAD_TARGET = './src/bad.js';
44+
45+
const testCases: ImportsTestCase[] = [
46+
{ name: 'simple string', mapping: GOOD_TARGET },
47+
{
48+
name: 'default fallback without matching condition',
49+
mapping: {
50+
'never': BAD_TARGET,
51+
'default': GOOD_TARGET,
52+
},
53+
},
54+
{
55+
name: 'development condition',
56+
mapping: {
57+
'development': GOOD_TARGET,
58+
'default': BAD_TARGET,
59+
},
60+
},
61+
{
62+
name: 'production condition',
63+
mapping: {
64+
'production': BAD_TARGET,
65+
'default': GOOD_TARGET,
66+
},
67+
},
68+
];
69+
70+
for (const testCase of testCases) {
71+
describe(testCase.name, () => {
72+
beforeEach(async () => {
73+
await setTargetMapping(harness, testCase.mapping);
74+
});
75+
76+
it('resolves to expected target', async () => {
77+
harness.useTarget('serve', {
78+
...BASE_OPTIONS,
79+
});
80+
81+
const { result, response } = await executeOnceAndFetch(harness, '/main.js');
82+
83+
expect(result?.success).toBeTrue();
84+
const output = await response?.text();
85+
expect(output).toContain('good-value');
86+
expect(output).not.toContain('bad-value');
87+
});
88+
});
89+
}
90+
});
91+
},
92+
);

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,12 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
540540
bundle: true,
541541
packages: 'bundle',
542542
assetNames: outputNames.media,
543-
conditions: ['es2020', 'es2015', 'module'],
543+
conditions: [
544+
'es2020',
545+
'es2015',
546+
'module',
547+
optimizationOptions.scripts ? 'production' : 'development',
548+
],
544549
resolveExtensions: ['.ts', '.tsx', '.mjs', '.js', '.cjs'],
545550
metafile: true,
546551
legalComments: options.extractLicenses ? 'none' : 'eof',

packages/angular/build/src/tools/esbuild/global-scripts.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function createGlobalScriptsBundleOptions(
6262
entryNames: initial ? outputNames.bundles : '[name]',
6363
assetNames: outputNames.media,
6464
mainFields: ['script', 'browser', 'main'],
65-
conditions: ['script'],
65+
conditions: ['script', optimizationOptions.scripts ? 'production' : 'development'],
6666
resolveExtensions: ['.mjs', '.js', '.cjs'],
6767
logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent',
6868
metafile: true,

packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function createStylesheetBundleOptions(
8282
preserveSymlinks: options.preserveSymlinks,
8383
external: options.externalDependencies,
8484
publicPath: options.publicPath,
85-
conditions: ['style', 'sass', 'less'],
85+
conditions: ['style', 'sass', 'less', options.optimization ? 'production' : 'development'],
8686
mainFields: ['style', 'sass'],
8787
// Unlike JS, CSS does not have implicit file extensions in the general case.
8888
// Preprocessor specific behavior is handled in each stylesheet language plugin.

0 commit comments

Comments
 (0)