Skip to content

Commit

Permalink
feat(plugin): default format to iife
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Plugin default format is now `iife` when Webpack's target is `web` & esbuild's target is below `esnext`
  • Loading branch information
privatenumber committed Feb 8, 2023
1 parent b052cdd commit a9e8e7e
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 6 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,19 @@ Read more about it in the [esbuild docs](https://esbuild.github.io/api/#target).

Here are some common configurations and custom options:

#### format
Type: `'iife' | 'cjs' | 'esm'`

Default:
- `iife` if both of these conditions are met:
- Webpack's [`target`](https://webpack.js.org/configuration/target/) is set to `web`
- esbuild's [`target`](#target-1) is not `esnext`
- `undefined` (no format conversion) otherwise

The default is `iife` when esbuild is configured to support a low target, because esbuild injects helper functions at the top of the code. On the web, having functions declared at the top of a script can pollute the global scope. In some cases, this can lead to a variable collision error. By setting `format: 'iife'`, esbuild wraps the helper functions in an [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) to prevent them from polluting the global.

Read more about it in the [esbuild docs](https://esbuild.github.io/api/#format).

#### minify
Type: `boolean`

Expand Down
36 changes: 31 additions & 5 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ class EsbuildPlugin {
private readonly transform: typeof defaultEsbuildTransform;

constructor(options: EsbuildPluginOptions = {}) {
const { implementation, ...remainingOptions } = options;
const {
implementation,
...remainingOptions
} = options;

if (implementation && typeof implementation.transform !== 'function') {
throw new TypeError(
`[${EsbuildPlugin.name}] implementation.transform must be an esbuild transform function. Received ${typeof implementation.transform}`,
Expand All @@ -35,24 +39,46 @@ class EsbuildPlugin {

this.transform = implementation?.transform ?? defaultEsbuildTransform;

this.options = remainingOptions;

const hasGranularMinificationConfig = granularMinifyConfigs.some(
minifyConfig => minifyConfig in options,
);

if (!hasGranularMinificationConfig) {
this.options.minify = true;
remainingOptions.minify = true;
}

this.options = remainingOptions;
}

apply(compiler: Compiler): void {
const { options } = this;
const meta = JSON.stringify({
name: 'esbuild-loader',
version,
options: this.options,
options,
});

if (!('format' in options)) {
const { target } = compiler.options;
const isWebTarget = (
Array.isArray(target)
? target.includes('web')
: target === 'web'
);
const wontGenerateHelpers = !options.target || (
Array.isArray(options.target)
? (
options.target.length === 1
&& options.target[0] === 'esnext'
)
: options.target === 'esnext'
);

if (isWebTarget && !wontGenerateHelpers) {
options.format = 'iife';
}
}

compiler.hooks.compilation.tap(pluginName, (compilation) => {
compilation.hooks.chunkHash.tap(pluginName, (_, hash) => hash.update(meta));

Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ export const minification = {
'/src/index.js': 'export default ( stringVal ) => { return stringVal }',
};

export const getHelpers = {
'/src/index.js': 'export default async () => {}',
};

export const legalComments = {
'/src/index.js': `
//! legal comment
Expand Down
75 changes: 74 additions & 1 deletion tests/specs/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ const assertMinified = (code: string) => {
expect(code).not.toMatch('return ');
};

const countIife = (code: string) => Array.from(code.matchAll(/\(\(\)=>\{/g)).length;

export default testSuite(({ describe }, webpack: typeof webpack4 | typeof webpack5) => {
const isWebpack4 = webpack.version?.startsWith('4.');

describe('Plugin', ({ test, describe }) => {
describe('Minify JS', ({ test }) => {
test('minify', async () => {
Expand Down Expand Up @@ -528,7 +532,7 @@ export default testSuite(({ describe }, webpack: typeof webpack4 | typeof webpac
size: () => Buffer.byteLength(content),
});

const built = await build({ '/src/index.js': '' }, (config) => {
const built = await build(fixtures.blank, (config) => {
configureEsbuildMinifyPlugin(config);

config.plugins!.push({
Expand Down Expand Up @@ -559,5 +563,74 @@ export default testSuite(({ describe }, webpack: typeof webpack4 | typeof webpac
built.fs.readFileSync('/dist/test.js', 'utf8'),
).toBe('1+1;\n');
});

describe('minify targets', ({ test }) => {
test('no iife for node', async () => {
const built = await build(
fixtures.getHelpers,
(config) => {
configureEsbuildMinifyPlugin(config, {
target: 'es2015',
});

config.target = isWebpack4 ? 'node' : ['node'];
delete config.output?.libraryTarget;
delete config.output?.libraryExport;
},
webpack,
);

expect(built.stats.hasWarnings()).toBe(false);
expect(built.stats.hasErrors()).toBe(false);

const code = built.fs.readFileSync('/dist/index.js', 'utf8').toString();
expect(code.startsWith('var ')).toBe(true);
});

test('no iife for web with high target (no helpers are added)', async () => {
const built = await build(
fixtures.getHelpers,
(config) => {
configureEsbuildMinifyPlugin(config);

config.target = isWebpack4 ? 'web' : ['web'];
delete config.output?.libraryTarget;
delete config.output?.libraryExport;
},
webpack,
);

expect(built.stats.hasWarnings()).toBe(false);
expect(built.stats.hasErrors()).toBe(false);

const code = built.fs.readFileSync('/dist/index.js', 'utf8').toString();
expect(code.startsWith('(()=>{var ')).toBe(false);
expect(countIife(code)).toBe(isWebpack4 ? 0 : 1);
});

test('iife for web & low target', async () => {
const built = await build(
fixtures.getHelpers,
(config) => {
configureEsbuildMinifyPlugin(config, {
target: 'es2015',
});

config.target = isWebpack4 ? 'web' : ['web'];
delete config.output?.libraryTarget;
delete config.output?.libraryExport;
},
webpack,
);

expect(built.stats.hasWarnings()).toBe(false);
expect(built.stats.hasErrors()).toBe(false);

const code = built.fs.readFileSync('/dist/index.js', 'utf8').toString();
expect(code.startsWith('(()=>{var ')).toBe(true);
expect(code.endsWith('})();\n')).toBe(true);
expect(countIife(code)).toBe(isWebpack4 ? 1 : 2);
});
});
});
});

0 comments on commit a9e8e7e

Please sign in to comment.