Skip to content

Commit

Permalink
feat(@angular/build): set development/production condition
Browse files Browse the repository at this point in the history
Ensures that we consistently set "development" for non-optimized and
"production" for optimized builds. This is consistent with other
bundlers (Vite/webpack/parcel/...).
  • Loading branch information
jkrems committed Oct 9, 2024
1 parent 7d883a1 commit c48d694
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 3 deletions.
54 changes: 54 additions & 0 deletions modules/testing/builder/src/dev_prod_mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @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 { BuilderHarness } from './builder-harness';

export const GOOD_TARGET = './src/good.js';
export const BAD_TARGET = './src/bad.js';

/** Setup project for use of conditional imports. */
export async function setupConditionImport(harness: BuilderHarness<unknown>) {
// Files that can be used as targets for the conditional import.
await harness.writeFile('src/good.ts', `export const VALUE = 'good-value';`);
await harness.writeFile('src/bad.ts', `export const VALUE = 'bad-value';`);

// Simple application file that accesses conditional code.
await harness.writeFile(
'src/main.ts',
`import {VALUE} from '#target';
console.log(VALUE);
export default 42 as any;
`,
);

// Ensure that good/bad can be resolved from tsconfig.
const tsconfig = JSON.parse(harness.readFile('src/tsconfig.app.json')) as TypeScriptConfig;
tsconfig.compilerOptions.moduleResolution = 'bundler';
tsconfig.files.push('good.ts', 'bad.ts');
await harness.writeFile('src/tsconfig.app.json', JSON.stringify(tsconfig));
}

/** Update package.json with the given mapping for #target. */
export async function setTargetMapping(harness: BuilderHarness<unknown>, mapping: unknown) {
await harness.writeFile(
'package.json',
JSON.stringify({
name: 'ng-test-app',
imports: {
'#target': mapping,
},
}),
);
}

interface TypeScriptConfig {
compilerOptions: {
moduleResolution: string;
};
files: string[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @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('Behavior: "conditional imports"', () => {
beforeEach(async () => {
await setupConditionImport(harness);
});

interface ImportsTestCase {
name: string;
mapping: unknown;
output?: string;
}

const GOOD_TARGET = './src/good.js';
const BAD_TARGET = './src/bad.js';

const testCases: ImportsTestCase[] = [
{ name: 'simple string', mapping: GOOD_TARGET },
{
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': GOOD_TARGET,
'default': BAD_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 testCases) {
describe(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',
});

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"');
});
});
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @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 { executeDevServer } from '../../index';
import { executeOnceAndFetch } from '../execute-fetch';
import { describeServeBuilder } from '../jasmine-helpers';
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';

describeServeBuilder(
executeDevServer,
DEV_SERVER_BUILDER_INFO,
(harness, setupTarget, isApplicationBuilder) => {
describe('Behavior: "conditional imports"', () => {
if (!isApplicationBuilder) {
it('requires esbuild', () => {
expect(true).toBeTrue();
});

return;
}

beforeEach(async () => {
setupTarget(harness);

await setupConditionImport(harness);
});

interface ImportsTestCase {
name: string;
mapping: unknown;
output?: string;
}

const GOOD_TARGET = './src/good.js';
const BAD_TARGET = './src/bad.js';

const testCases: ImportsTestCase[] = [
{ name: 'simple string', mapping: GOOD_TARGET },
{
name: 'default fallback without matching condition',
mapping: {
'never': BAD_TARGET,
'default': GOOD_TARGET,
},
},
{
name: 'development condition',
mapping: {
'development': GOOD_TARGET,
'default': BAD_TARGET,
},
},
{
name: 'production condition',
mapping: {
'production': BAD_TARGET,
'default': GOOD_TARGET,
},
},
{
name: 'browser condition (in browser)',
mapping: {
'browser': GOOD_TARGET,
'default': BAD_TARGET,
},
},
];

for (const testCase of testCases) {
describe(testCase.name, () => {
beforeEach(async () => {
await setTargetMapping(harness, testCase.mapping);
});

it('resolves to expected target', async () => {
harness.useTarget('serve', {
...BASE_OPTIONS,
});

const { result, response } = await executeOnceAndFetch(harness, '/main.js');

expect(result?.success).toBeTrue();
const output = await response?.text();
expect(output).toContain('good-value');
expect(output).not.toContain('bad-value');
});
});
}
});
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,12 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
bundle: true,
packages: 'bundle',
assetNames: outputNames.media,
conditions: ['es2020', 'es2015', 'module'],
conditions: [
'es2020',
'es2015',
'module',
optimizationOptions.scripts ? 'production' : 'development',
],
resolveExtensions: ['.ts', '.tsx', '.mjs', '.js', '.cjs'],
metafile: true,
legalComments: options.extractLicenses ? 'none' : 'eof',
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/build/src/tools/esbuild/global-scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function createGlobalScriptsBundleOptions(
entryNames: initial ? outputNames.bundles : '[name]',
assetNames: outputNames.media,
mainFields: ['script', 'browser', 'main'],
conditions: ['script'],
conditions: ['script', optimizationOptions.scripts ? 'production' : 'development'],
resolveExtensions: ['.mjs', '.js', '.cjs'],
logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent',
metafile: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function createStylesheetBundleOptions(
preserveSymlinks: options.preserveSymlinks,
external: options.externalDependencies,
publicPath: options.publicPath,
conditions: ['style', 'sass', 'less'],
conditions: ['style', 'sass', 'less', options.optimization ? 'production' : 'development'],
mainFields: ['style', 'sass'],
// Unlike JS, CSS does not have implicit file extensions in the general case.
// Preprocessor specific behavior is handled in each stylesheet language plugin.
Expand Down

0 comments on commit c48d694

Please sign in to comment.