Skip to content

Commit

Permalink
refactor: add outExtension to browser-esbuild as an internal option
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dgp1130 committed Apr 21, 2023
1 parent 07d7276 commit 399f489
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ function createCodeBundleOptions(
sourcemapOptions,
tsconfig,
outputNames,
outExtension,
fileReplacements,
externalDependencies,
preserveSymlinks,
Expand Down Expand Up @@ -359,6 +360,7 @@ function createCodeBundleOptions(
minify: optimizationOptions.scripts,
pure: ['forwardRef'],
outdir: workspaceRoot,
outExtension: outExtension ? { '.js': `.${outExtension}` } : undefined,
sourcemap: sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true),
splitting: true,
tsconfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ interface InternalOptions {
* name.
*/
entryPoints?: Set<string>;

/** File extension to use for the generated output files. */
outExtension?: 'js' | 'mjs';
}

/** Full set of options for `browser-esbuild` builder. */
Expand Down Expand Up @@ -166,6 +169,7 @@ export async function normalizeOptions(
externalDependencies,
extractLicenses,
inlineStyleLanguage = 'css',
outExtension,
poll,
polyfills,
preserveSymlinks,
Expand Down Expand Up @@ -202,6 +206,7 @@ export async function normalizeOptions(
entryPoints,
optimizationOptions,
outputPath,
outExtension,
sourcemapOptions,
tsconfig,
projectRoot,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @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.io/license
*/

import { buildEsbuildBrowserInternal } from '../../index';
import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';

describeBuilder(buildEsbuildBrowserInternal, BROWSER_BUILDER_INFO, (harness) => {
describe('Option: "outExtension"', () => {
it('outputs `.js` files when explicitly set to "js"', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
main: 'src/main.ts',
outExtension: 'js',
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();

// Should generate the correct file extension.
harness.expectFile('dist/main.js').toExist();
expect(harness.hasFile('dist/main.mjs')).toBeFalse();

// Index page should link to the correct file extension.
const indexContents = harness.readFile('dist/index.html');
expect(indexContents).toContain('src="main.js"');
});

it('outputs `.mjs` files when set to "mjs"', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
main: 'src/main.ts',
outExtension: 'mjs',
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();

// Should generate the correct file extension.
harness.expectFile('dist/main.mjs').toExist();
expect(harness.hasFile('dist/main.js')).toBeFalse();

// Index page should link to the correct file extension.
const indexContents = harness.readFile('dist/index.html');
expect(indexContents).toContain('src="main.mjs"');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ export async function augmentIndexHtml(
// Also, non entrypoints need to be loaded as no module as they can contain problematic code.
scripts.set(file, isModule);
break;
case '.mjs':
if (!isModule) {
// It would be very confusing to link an `*.mjs` file in a non-module script context,
// so we disallow it entirely.
throw new Error('`.mjs` files *must* set `isModule` to `true`.');
}
scripts.set(file, true /* isModule */);
break;
case '.css':
stylesheets.add(file);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,26 @@ describe('augment-index-html', () => {
<app-root></app-root>
`);
});

it('should add `.mjs` script tags', async () => {
const { content } = await augmentIndexHtml({
...indexGeneratorOptions,
files: [{ file: 'main.mjs', extension: '.mjs', name: 'main' }],
entrypoints: [['main', true /* isModule */]],
});

expect(content).toContain('<script src="main.mjs" type="module"></script>');
});

it('should reject non-module `.mjs` scripts', async () => {
const options: AugmentIndexHtmlOptions = {
...indexGeneratorOptions,
files: [{ file: 'main.mjs', extension: '.mjs', name: 'main' }],
entrypoints: [['main', false /* isModule */]],
};

await expectAsync(augmentIndexHtml(options)).toBeRejectedWithError(
'`.mjs` files *must* set `isModule` to `true`.',
);
});
});

0 comments on commit 399f489

Please sign in to comment.