Skip to content

Commit c045c99

Browse files
dgp1130angular-robot[bot]
authored andcommitted
refactor: add outExtension to browser-esbuild as an internal option
The `outExtension` option allows users to generate `*.mjs` files, which is useful for forcing ESM execution in Node under certain use cases. The option is limited to `*.js` and `*.mjs` files to constrain it to expected values. `*.cjs` could theoretically be useful in some specific situations, but `browser-esbuild` does not support that output format anyways, so it is not included in the type. I also updated `index.html` generation, which will correctly insert a `<script />` tag with the `*.mjs` extension. I opted to explicitly ban a "non-module" `*.mjs` file, since that would be very counterintuitive and I can't think of a valid use case for it.
1 parent 0d4a40f commit c045c99

File tree

5 files changed

+89
-0
lines changed

5 files changed

+89
-0
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ function createCodeBundleOptions(
331331
sourcemapOptions,
332332
tsconfig,
333333
outputNames,
334+
outExtension,
334335
fileReplacements,
335336
externalDependencies,
336337
preserveSymlinks,
@@ -359,6 +360,7 @@ function createCodeBundleOptions(
359360
minify: optimizationOptions.scripts,
360361
pure: ['forwardRef'],
361362
outdir: workspaceRoot,
363+
outExtension: outExtension ? { '.js': `.${outExtension}` } : undefined,
362364
sourcemap: sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true),
363365
splitting: true,
364366
tsconfig,

packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ interface InternalOptions {
2929
* name.
3030
*/
3131
entryPoints?: Set<string>;
32+
33+
/** File extension to use for the generated output files. */
34+
outExtension?: 'js' | 'mjs';
3235
}
3336

3437
/** Full set of options for `browser-esbuild` builder. */
@@ -166,6 +169,7 @@ export async function normalizeOptions(
166169
externalDependencies,
167170
extractLicenses,
168171
inlineStyleLanguage = 'css',
172+
outExtension,
169173
poll,
170174
polyfills,
171175
preserveSymlinks,
@@ -202,6 +206,7 @@ export async function normalizeOptions(
202206
entryPoints,
203207
optimizationOptions,
204208
outputPath,
209+
outExtension,
205210
sourcemapOptions,
206211
tsconfig,
207212
projectRoot,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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.io/license
7+
*/
8+
9+
import { buildEsbuildBrowserInternal } from '../../index';
10+
import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
11+
12+
describeBuilder(buildEsbuildBrowserInternal, BROWSER_BUILDER_INFO, (harness) => {
13+
describe('Option: "outExtension"', () => {
14+
it('outputs `.js` files when explicitly set to "js"', async () => {
15+
harness.useTarget('build', {
16+
...BASE_OPTIONS,
17+
main: 'src/main.ts',
18+
outExtension: 'js',
19+
});
20+
21+
const { result } = await harness.executeOnce();
22+
expect(result?.success).toBeTrue();
23+
24+
// Should generate the correct file extension.
25+
harness.expectFile('dist/main.js').toExist();
26+
expect(harness.hasFile('dist/main.mjs')).toBeFalse();
27+
28+
// Index page should link to the correct file extension.
29+
const indexContents = harness.readFile('dist/index.html');
30+
expect(indexContents).toContain('src="main.js"');
31+
});
32+
33+
it('outputs `.mjs` files when set to "mjs"', async () => {
34+
harness.useTarget('build', {
35+
...BASE_OPTIONS,
36+
main: 'src/main.ts',
37+
outExtension: 'mjs',
38+
});
39+
40+
const { result } = await harness.executeOnce();
41+
expect(result?.success).toBeTrue();
42+
43+
// Should generate the correct file extension.
44+
harness.expectFile('dist/main.mjs').toExist();
45+
expect(harness.hasFile('dist/main.js')).toBeFalse();
46+
47+
// Index page should link to the correct file extension.
48+
const indexContents = harness.readFile('dist/index.html');
49+
expect(indexContents).toContain('src="main.mjs"');
50+
});
51+
});
52+
});

packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ export async function augmentIndexHtml(
7979
// Also, non entrypoints need to be loaded as no module as they can contain problematic code.
8080
scripts.set(file, isModule);
8181
break;
82+
case '.mjs':
83+
if (!isModule) {
84+
// It would be very confusing to link an `*.mjs` file in a non-module script context,
85+
// so we disallow it entirely.
86+
throw new Error('`.mjs` files *must* set `isModule` to `true`.');
87+
}
88+
scripts.set(file, true /* isModule */);
89+
break;
8290
case '.css':
8391
stylesheets.add(file);
8492
break;

packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html_spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,26 @@ describe('augment-index-html', () => {
163163
<app-root></app-root>
164164
`);
165165
});
166+
167+
it('should add `.mjs` script tags', async () => {
168+
const { content } = await augmentIndexHtml({
169+
...indexGeneratorOptions,
170+
files: [{ file: 'main.mjs', extension: '.mjs', name: 'main' }],
171+
entrypoints: [['main', true /* isModule */]],
172+
});
173+
174+
expect(content).toContain('<script src="main.mjs" type="module"></script>');
175+
});
176+
177+
it('should reject non-module `.mjs` scripts', async () => {
178+
const options: AugmentIndexHtmlOptions = {
179+
...indexGeneratorOptions,
180+
files: [{ file: 'main.mjs', extension: '.mjs', name: 'main' }],
181+
entrypoints: [['main', false /* isModule */]],
182+
};
183+
184+
await expectAsync(augmentIndexHtml(options)).toBeRejectedWithError(
185+
'`.mjs` files *must* set `isModule` to `true`.',
186+
);
187+
});
166188
});

0 commit comments

Comments
 (0)