Skip to content

Commit c48d694

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 7d883a1 commit c48d694

File tree

6 files changed

+257
-3
lines changed

6 files changed

+257
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
export default 42 as any;
26+
`,
27+
);
28+
29+
// Ensure that good/bad can be resolved from tsconfig.
30+
const tsconfig = JSON.parse(harness.readFile('src/tsconfig.app.json')) as TypeScriptConfig;
31+
tsconfig.compilerOptions.moduleResolution = 'bundler';
32+
tsconfig.files.push('good.ts', 'bad.ts');
33+
await harness.writeFile('src/tsconfig.app.json', JSON.stringify(tsconfig));
34+
}
35+
36+
/** Update package.json with the given mapping for #target. */
37+
export async function setTargetMapping(harness: BuilderHarness<unknown>, mapping: unknown) {
38+
await harness.writeFile(
39+
'package.json',
40+
JSON.stringify({
41+
name: 'ng-test-app',
42+
imports: {
43+
'#target': mapping,
44+
},
45+
}),
46+
);
47+
}
48+
49+
interface TypeScriptConfig {
50+
compilerOptions: {
51+
moduleResolution: string;
52+
};
53+
files: string[];
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
output?: string;
26+
}
27+
28+
const GOOD_TARGET = './src/good.js';
29+
const BAD_TARGET = './src/bad.js';
30+
31+
const testCases: ImportsTestCase[] = [
32+
{ name: 'simple string', mapping: GOOD_TARGET },
33+
{
34+
name: 'default fallback without matching condition',
35+
mapping: {
36+
'never': BAD_TARGET,
37+
'default': GOOD_TARGET,
38+
},
39+
},
40+
{
41+
name: 'development condition',
42+
mapping: {
43+
'development': BAD_TARGET,
44+
'default': GOOD_TARGET,
45+
},
46+
},
47+
{
48+
name: 'production condition',
49+
mapping: {
50+
'production': GOOD_TARGET,
51+
'default': BAD_TARGET,
52+
},
53+
},
54+
{
55+
name: 'browser condition (in browser)',
56+
mapping: {
57+
'browser': GOOD_TARGET,
58+
'default': BAD_TARGET,
59+
},
60+
},
61+
{
62+
name: 'browser condition (in server)',
63+
output: 'server/main.server.mjs',
64+
mapping: {
65+
'browser': BAD_TARGET,
66+
'default': GOOD_TARGET,
67+
},
68+
},
69+
];
70+
71+
for (const testCase of testCases) {
72+
describe(testCase.name, () => {
73+
beforeEach(async () => {
74+
await setTargetMapping(harness, testCase.mapping);
75+
});
76+
77+
it('resolves to expected target', async () => {
78+
harness.useTarget('build', {
79+
...BASE_OPTIONS,
80+
optimization: true,
81+
ssr: true,
82+
server: 'src/main.ts',
83+
});
84+
85+
const { result } = await harness.executeOnce();
86+
87+
expect(result?.success).toBeTrue();
88+
const outputFile = `dist/${testCase.output ?? 'browser/main.js'}`;
89+
harness.expectFile(outputFile).content.toContain('"good-value"');
90+
harness.expectFile(outputFile).content.not.toContain('"bad-value"');
91+
});
92+
});
93+
}
94+
});
95+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
output?: string;
41+
}
42+
43+
const GOOD_TARGET = './src/good.js';
44+
const BAD_TARGET = './src/bad.js';
45+
46+
const testCases: ImportsTestCase[] = [
47+
{ name: 'simple string', mapping: GOOD_TARGET },
48+
{
49+
name: 'default fallback without matching condition',
50+
mapping: {
51+
'never': BAD_TARGET,
52+
'default': GOOD_TARGET,
53+
},
54+
},
55+
{
56+
name: 'development condition',
57+
mapping: {
58+
'development': GOOD_TARGET,
59+
'default': BAD_TARGET,
60+
},
61+
},
62+
{
63+
name: 'production condition',
64+
mapping: {
65+
'production': BAD_TARGET,
66+
'default': GOOD_TARGET,
67+
},
68+
},
69+
{
70+
name: 'browser condition (in browser)',
71+
mapping: {
72+
'browser': GOOD_TARGET,
73+
'default': BAD_TARGET,
74+
},
75+
},
76+
];
77+
78+
for (const testCase of testCases) {
79+
describe(testCase.name, () => {
80+
beforeEach(async () => {
81+
await setTargetMapping(harness, testCase.mapping);
82+
});
83+
84+
it('resolves to expected target', async () => {
85+
harness.useTarget('serve', {
86+
...BASE_OPTIONS,
87+
});
88+
89+
const { result, response } = await executeOnceAndFetch(harness, '/main.js');
90+
91+
expect(result?.success).toBeTrue();
92+
const output = await response?.text();
93+
expect(output).toContain('good-value');
94+
expect(output).not.toContain('bad-value');
95+
});
96+
});
97+
}
98+
});
99+
},
100+
);

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,12 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
564564
bundle: true,
565565
packages: 'bundle',
566566
assetNames: outputNames.media,
567-
conditions: ['es2020', 'es2015', 'module'],
567+
conditions: [
568+
'es2020',
569+
'es2015',
570+
'module',
571+
optimizationOptions.scripts ? 'production' : 'development',
572+
],
568573
resolveExtensions: ['.ts', '.tsx', '.mjs', '.js', '.cjs'],
569574
metafile: true,
570575
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)