Skip to content

Commit f2caf84

Browse files
committed
fix: minify .bundle assets with terser-webpack-plugin 5.6.0 and newer
Since 5.6.0 the plugin only passes assets accepted by `terserMinify.filter` (`.js`, `.cjs`, `.mjs`) to terser, so Re.Pack's `index.bundle` and `*.chunk.bundle` are dropped before minification with no error or warning. Configure a `minify` wrapper instead. A plain function carries no filter, so every asset reaches terser on old and new plugin versions alike. The wrapper forwards `getMinimizerVersion` so terser's version stays in the chunk hash.
1 parent ed1003f commit f2caf84

3 files changed

Lines changed: 119 additions & 0 deletions

File tree

‎.changeset/wrap-terser-minify.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@callstack/repack": patch
3+
---
4+
5+
Fix production bundles shipping unminified with `terser-webpack-plugin` 5.6.0 and newer, which only minifies `.js` assets by default and silently skipped Re.Pack's `.bundle` output.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { importDefaultESM } from '../../../../helpers/index.js';
2+
import { getMinimizerConfig } from '../getMinimizerConfig.js';
3+
4+
jest.mock('../../../../helpers/index.js', () => ({
5+
...jest.requireActual('../../../../helpers/index.js'),
6+
importDefaultESM: jest.fn(),
7+
}));
8+
9+
const importDefaultESMMock = jest.mocked(importDefaultESM);
10+
11+
// the filter `terser-webpack-plugin` puts on its own minifiers since 5.6.0
12+
const acceptsJsOnly = (name: string) => /\.[cm]?js(\?.*)?$/i.test(name);
13+
14+
// the shape `terser-webpack-plugin` normalizes `minify` and `terserOptions` into
15+
type NormalizedPlugin = {
16+
options: {
17+
minimizer: {
18+
implementation: ((
19+
input: Record<string, string>,
20+
sourceMap: undefined,
21+
minimizerOptions: unknown,
22+
extractComments: boolean
23+
) => Promise<{ code: string }>) & {
24+
filter?: (name: string) => boolean;
25+
getMinimizerVersion: () => string | undefined;
26+
};
27+
options: unknown;
28+
};
29+
};
30+
};
31+
32+
// an asset is skipped when the configured minifier declares a `filter` rejecting its name
33+
function isMinified(minifier: { filter?: (name: string) => boolean }) {
34+
return (
35+
typeof minifier.filter !== 'function' || minifier.filter('index.bundle')
36+
);
37+
}
38+
39+
describe('getMinimizerConfig', () => {
40+
it('should minify .bundle assets with a plugin that only accepts .js', async () => {
41+
const PluginMock = Object.assign(jest.fn(), {
42+
terserMinify: Object.assign(jest.fn(), { filter: acceptsJsOnly }),
43+
});
44+
importDefaultESMMock.mockResolvedValue(PluginMock);
45+
46+
await getMinimizerConfig('webpack', '/project');
47+
48+
const { minify } = PluginMock.mock.calls[0][0];
49+
const implementation = minify ?? PluginMock.terserMinify;
50+
51+
expect(implementation.filter).toBeUndefined();
52+
expect(isMinified(implementation)).toBe(true);
53+
});
54+
55+
it('should run terser on a .bundle asset after worker serialization', async () => {
56+
importDefaultESMMock.mockImplementation(async (path) => require(path));
57+
58+
const [minimizer] = await getMinimizerConfig('webpack', process.cwd());
59+
const { implementation, options } = (
60+
minimizer as unknown as NormalizedPlugin
61+
).options.minimizer;
62+
63+
// the plugin re-evaluates the minifier from its source inside a worker
64+
const deserialized = new Function('require', `return ${implementation}`)(
65+
require
66+
);
67+
const { code } = await deserialized(
68+
{ 'index.bundle': 'const answer = 40 + 2;' },
69+
undefined,
70+
options,
71+
false
72+
);
73+
74+
expect(code).toBe('const answer=42;');
75+
});
76+
77+
it('should report the terser version the built-in minifier reports', async () => {
78+
importDefaultESMMock.mockImplementation(async (path) => require(path));
79+
80+
const [minimizer] = await getMinimizerConfig('webpack', process.cwd());
81+
const { implementation } = (minimizer as unknown as NormalizedPlugin)
82+
.options.minimizer;
83+
const { terserMinify } = require('terser-webpack-plugin');
84+
85+
// the plugin puts this in the chunk hash, so losing it would stale the cache
86+
expect(implementation.getMinimizerVersion()).toEqual(expect.any(String));
87+
expect(implementation.getMinimizerVersion()).toBe(
88+
terserMinify.getMinimizerVersion()
89+
);
90+
});
91+
});

‎packages/repack/src/commands/common/config/getMinimizerConfig.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,34 @@ async function getTerserPlugin(rootDir: string) {
1616
return plugin;
1717
}
1818

19+
type TerserMinifyArgs = Parameters<(typeof TerserPlugin)['terserMinify']>;
20+
21+
// since 5.6.0 the plugin's own `terserMinify` carries a `.filter` that rejects `.bundle`
22+
// assets; a wrapper carries none. It runs in a worker, so it must stay self-contained.
23+
function repackTerserMinify(
24+
input: TerserMinifyArgs[0],
25+
sourceMap: TerserMinifyArgs[1],
26+
minimizerOptions: TerserMinifyArgs[2],
27+
extractComments: TerserMinifyArgs[3]
28+
) {
29+
return require('terser-webpack-plugin').terserMinify(
30+
input,
31+
sourceMap,
32+
minimizerOptions,
33+
extractComments
34+
);
35+
}
36+
37+
// read on the main thread only, to keep terser's version in the chunk hash
38+
repackTerserMinify.getMinimizerVersion = () =>
39+
require('terser-webpack-plugin').terserMinify.getMinimizerVersion?.();
40+
1941
async function getTerserConfig(rootDir: string) {
2042
const TerserPlugin = await getTerserPlugin(rootDir);
2143
return new TerserPlugin({
2244
test: /\.(js)?bundle(\?.*)?$/i,
2345
extractComments: false,
46+
minify: repackTerserMinify,
2447
terserOptions: {
2548
format: { comments: false },
2649
},

0 commit comments

Comments
 (0)