Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -540,7 +540,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