Skip to content

Commit 6727922

Browse files
feat: minify JSON assets under futureDefaults (#22286)
* feat: minify JSON assets under futureDefaults Add `optimization.minimizeOptions.json`, which dispatches JSON assets to minimizer-webpack-plugin's `jsonMinify`. It defaults to `true` with `experiments.futureDefaults` and steps aside for a user JSON minimizer. * fix: keep JSON tokens exact and require both matchers to claim Minify JSON assets with webpack's own whitespace-only jsonMinify so large integers and exponents survive, and leave non-JSON .json assets as written. A configured minimizer now claims a type only when its test and include both match. * test: cover jsonMinify directly The default minimizer runs it in a worker, where integration cases do not collect coverage, so exercise it the way jsMinify.unittest.js does. * test: keep coverage instrumentation out of jsonMinify The minimizer ships it to its worker pool as source, where the injected coverage counters are undefined, like the other worker minifiers. * test: check minimize-json assets from the config futureDefaults emits an ESM bundle, which reaches fs through createRequire and so fails on Node 10; read the emitted files in afterEmit instead. * fix: match JSON asset names carrying a fragment Use the same name pattern #22277 gave the JS, CSS and HTML minifiers, so `data.json#hash` is minified too.
1 parent ab83631 commit 6727922

18 files changed

Lines changed: 335 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": minor
3+
---
4+
5+
Add `optimization.minimizeOptions.json`, on by default with `futureDefaults`.

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ This is the canonical repository map. **When you add, rename or remove a top-lev
8484
- `syntax.js` reaches `parser` and `printer` through getters: parsing never loads the printer, printing never the parser.
8585
- `syntax-parser.js` — tokenizer, acorn-derived core and every production in one file (any parse reaches the productions, and the coming struct-of-arrays rewrite moves node creation through them). `regexp.js` (pattern validator) is the one on-demand piece, reached only for a pattern the host engine rejected — never `require` it from a build path.
8686
- `syntax-printer.js` — prints JS back out. `jsMinify.js` (the `minify` the default minimizer dispatches JS to) goes through it rather than terser's published entry, because that loader reads terser's sources — letting a phase webpack implements replace terser's method. Each phase taken over is one less thing terser does. A phase states what it reads with `supports`; a minifier that moved any of it, or a runtime that can't import those sources, keeps its own. Add phases to the `PHASES` list there and nowhere else, each writing byte-for-byte what it replaced.
87-
- `lib/json/` — JSON modules.
87+
- `lib/json/` — JSON modules, and `jsonMinify`, which strips the whitespace between a JSON asset's tokens for the default minimizer.
8888
- `lib/library/` — UMD/AMD/ESM/CommonJS library formats, and the deprecated `LibraryTemplatePlugin` (old two-argument API).
8989
- `lib/loaders/` — loader execution (vendored loader-runner): pitching/normal iteration and loader loading, plus `LoaderOptionsPlugin` and `LoaderTargetPlugin`, which feed the loader context.
9090
- `lib/logging/` — Logger API, console formatting, and `ProgressPlugin` (reports through it). `lib/ProgressPlugin` stays a re-export (webpack-stream requires it).

‎jest.config.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const config = {
4949
"\\.runtime\\.js$",
5050
// Serialized to the minimizer's worker pool as source (like runtime code):
5151
// coverage instrumentation would inject counters the worker can't resolve.
52-
"[\\\\/]lib[\\\\/](?:css[\\\\/]cssMinify|html[\\\\/]htmlMinify|javascript[\\\\/]jsMinify)\\.js$",
52+
"[\\\\/]lib[\\\\/](?:css[\\\\/]cssMinify|html[\\\\/]htmlMinify|javascript[\\\\/]jsMinify|json[\\\\/]jsonMinify)\\.js$",
5353
"<rootDir>/test",
5454
"<rootDir>/schemas",
5555
"<rootDir>/examples",

‎lib/config/defaults.js‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2517,6 +2517,7 @@ const applyOptimizationDefaults = (
25172517
);
25182518
F(minimizeOptions, "css", () => ({}));
25192519
F(minimizeOptions, "html", () => ({}));
2520+
D(minimizeOptions, "json", futureDefaults);
25202521
}
25212522
A(optimization, "minimizer", () => [
25222523
{
@@ -2557,7 +2558,10 @@ const applyOptimizationDefaults = (
25572558
Boolean(html) &&
25582559
!claimedByOtherMinimizer.html &&
25592560
minimizeByType.html !== false;
2560-
if (!minifyJs && !minifyCss && !minifyHtml) return;
2561+
// `=== true` rather than `!== false`: outside `futureDefaults`, JSON is opt-in.
2562+
const minifyJson =
2563+
minimizeByType.json === true && !claimedByOtherMinimizer.json;
2564+
if (!minifyJs && !minifyCss && !minifyHtml && !minifyJson) return;
25612565
// An asset name keeps the query and the fragment its filename template
25622566
// wrote (`[query][fragment]`, `main.js#[contenthash]`), so both may follow.
25632567
/**
@@ -2566,15 +2570,15 @@ const applyOptimizationDefaults = (
25662570
*/
25672571
const assetNameTest = (extensions) =>
25682572
new RegExp(`^[^?#]*\\.(?:${extensions.join("|")})(?:[?#].*)?$`, "i");
2569-
if (minifyJs && !minifyCss && !minifyHtml) {
2573+
if (minifyJs && !minifyCss && !minifyHtml && !minifyJson) {
25702574
new TerserPlugin({
25712575
test: assetNameTest(["[cm]?js"]),
25722576
minify: require("../javascript/jsMinify"),
25732577
minimizerOptions: terserOptions
25742578
}).apply(/** @type {EXPECTED_ANY} */ (compiler));
25752579
return;
25762580
}
2577-
/** @type {(typeof import("../javascript/jsMinify") | typeof import("../css/cssMinify") | typeof import("../html/htmlMinify"))[]} */
2581+
/** @type {(typeof import("../javascript/jsMinify") | typeof import("../css/cssMinify") | typeof import("../html/htmlMinify") | typeof import("../json/jsonMinify"))[]} */
25782582
const minify = [];
25792583
/** @type {EXPECTED_OBJECT[]} */
25802584
const minimizerOptions = [];
@@ -2630,6 +2634,11 @@ const applyOptimizationDefaults = (
26302634
});
26312635
extensions.push("html");
26322636
}
2637+
if (minifyJson) {
2638+
minify.push(require("../json/jsonMinify"));
2639+
minimizerOptions.push({});
2640+
extensions.push("json");
2641+
}
26332642
new TerserPlugin({
26342643
test: assetNameTest(extensions),
26352644
minify,

‎lib/config/getClaimedAssetTypes.js‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,12 @@ const claims = (candidate, file) => {
2626
// so reading those as minimizers would disable minification.
2727
const { options } = /** @type {{ options?: MinimizerOptions }} */ (candidate);
2828
if (!options || !options.minimizer) return false;
29-
// `test` and `include` say which types it handles, while `exclude` only
30-
// narrows the files within them: honoring that narrowing would let a minimizer
31-
// scoped to a subdirectory claim nothing, and webpack would then race it.
29+
// `test` and `include` say which types it handles, and the minimizer takes a
30+
// file only when both match. `exclude` merely narrows the files within them:
31+
// honoring it would let a subdirectory-scoped minimizer claim nothing.
3232
const { test, include } = options;
33-
return (
34-
(test !== undefined && matchObject({ test }, file)) ||
35-
(include !== undefined && matchObject({ include }, file))
36-
);
33+
if (test === undefined && include === undefined) return false;
34+
return matchObject({ test, include }, file);
3735
};
3836

3937
/**
@@ -44,12 +42,13 @@ const claims = (candidate, file) => {
4442
* matcher rather than resolving it per emitted asset, so a minimizer restricted
4543
* to some of a type's files still claims the whole type.
4644
* @param {Compiler} compiler the compiler
47-
* @returns {{ css: boolean, html: boolean }} types another minimizer handles
45+
* @returns {{ css: boolean, html: boolean, json: boolean }} types another minimizer handles
4846
*/
4947
module.exports = (compiler) => {
5048
const { optimization, plugins } = compiler.options;
5149
let css = false;
5250
let html = false;
51+
let json = false;
5352
// Both lists are normalized arrays with falsy entries already filtered out,
5453
// and `minimizer` was filled in by the defaults before any plugin applies.
5554
const minimizer =
@@ -58,7 +57,8 @@ module.exports = (compiler) => {
5857
for (const candidate of [...minimizer, ...plugins]) {
5958
if (!css) css = claims(candidate, "file.css");
6059
if (!html) html = claims(candidate, "file.html");
61-
if (css && html) break;
60+
if (!json) json = claims(candidate, "file.json");
61+
if (css && html && json) break;
6262
}
63-
return { css, html };
63+
return { css, html, json };
6464
};

‎lib/json/jsonMinify.js‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author sheo13666q @sheo13666q
4+
*/
5+
6+
"use strict";
7+
8+
/**
9+
* A `minify` function for `minimizer-webpack-plugin` that drops the whitespace
10+
* between a JSON asset's tokens and keeps every token as written. Source that is
11+
* not valid JSON is returned unchanged rather than failing the build.
12+
* @param {{ [file: string]: string | Buffer }} input a single `{ filename: code }` entry; a `Buffer` is read as UTF-8
13+
* @returns {Promise<{ code: string }>} the minified JSON
14+
*/
15+
const jsonMinify = async (input) => {
16+
// WHY: the body is self-contained because the plugin ships this function to
17+
// its worker pool as source. Re-serializing through `JSON.parse` would round
18+
// `9007199254740993` to `…992` and turn `1e400` into `null`, so only the
19+
// whitespace goes; `JSON.parse` is asked solely whether the source is JSON.
20+
const [[, source]] = Object.entries(input);
21+
const code = typeof source === "string" ? source : source.toString("utf8");
22+
try {
23+
JSON.parse(code);
24+
} catch (_err) {
25+
return { code };
26+
}
27+
let result = "";
28+
let start = 0;
29+
let inString = false;
30+
for (let i = 0; i < code.length; i++) {
31+
const charCode = code.charCodeAt(i);
32+
if (inString) {
33+
// A backslash always escapes the one character after it.
34+
if (charCode === 92) i++;
35+
else if (charCode === 34) inString = false;
36+
} else if (charCode === 34) {
37+
inString = true;
38+
} else if (
39+
charCode === 32 ||
40+
charCode === 10 ||
41+
charCode === 13 ||
42+
charCode === 9
43+
) {
44+
result += code.slice(start, i);
45+
start = i + 1;
46+
}
47+
}
48+
return { code: result + code.slice(start) };
49+
};
50+
51+
// Worker-safe (the body reaches nothing outside itself), so it may run in the
52+
// shared worker-thread pool alongside terser.
53+
jsonMinify.supportsWorkerThreads = () => true;
54+
55+
/**
56+
* The language this minifies, for a caller dispatching source that carries no
57+
* filename of its own.
58+
* @returns {string[]} the languages
59+
*/
60+
jsonMinify.getTypes = () => ["json"];
61+
62+
/**
63+
* @param {string} name asset filename
64+
* @returns {boolean} true for JSON assets
65+
*/
66+
jsonMinify.filter = (name) => /^[^?#]*\.json(?:[?#].*)?$/i.test(name);
67+
68+
module.exports = jsonMinify;

‎schemas/WebpackOptions.check.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎schemas/WebpackOptions.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4356,6 +4356,11 @@
43564356
"$ref": "#/definitions/OptimizationMinimizeJavascript"
43574357
}
43584358
]
4359+
},
4360+
"json": {
4361+
"description": "Minimize JSON assets by re-serializing them without whitespace (defaults to `true` with `experiments.futureDefaults`, otherwise `false`).",
4362+
"type": "boolean",
4363+
"added": "5.112.0"
43594364
}
43604365
},
43614366
"added": "5.110.0"

‎test/__snapshots__/Cli.basictest.js.snap‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11884,6 +11884,19 @@ Object {
1188411884
"multiple": false,
1188511885
"simpleType": "boolean",
1188611886
},
11887+
"optimization-minimize-json": Object {
11888+
"configs": Array [
11889+
Object {
11890+
"description": "Minimize JSON assets by re-serializing them without whitespace (defaults to \`true\` with \`experiments.futureDefaults\`, otherwise \`false\`).",
11891+
"multiple": false,
11892+
"path": "optimization.minimize.json",
11893+
"type": "boolean",
11894+
},
11895+
],
11896+
"description": "Minimize JSON assets by re-serializing them without whitespace (defaults to \`true\` with \`experiments.futureDefaults\`, otherwise \`false\`).",
11897+
"multiple": false,
11898+
"simpleType": "boolean",
11899+
},
1188711900
"optimization-minimize-options-css": Object {
1188811901
"configs": Array [
1188911902
Object {
@@ -12575,6 +12588,19 @@ Object {
1257512588
"multiple": false,
1257612589
"simpleType": "boolean",
1257712590
},
12591+
"optimization-minimize-options-json": Object {
12592+
"configs": Array [
12593+
Object {
12594+
"description": "Minimize JSON assets by re-serializing them without whitespace (defaults to \`true\` with \`experiments.futureDefaults\`, otherwise \`false\`).",
12595+
"multiple": false,
12596+
"path": "optimization.minimizeOptions.json",
12597+
"type": "boolean",
12598+
},
12599+
],
12600+
"description": "Minimize JSON assets by re-serializing them without whitespace (defaults to \`true\` with \`experiments.futureDefaults\`, otherwise \`false\`).",
12601+
"multiple": false,
12602+
"simpleType": "boolean",
12603+
},
1257812604
"optimization-module-ids": Object {
1257912605
"configs": Array [
1258012606
Object {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "webpack",
3+
"list": [1, 2, 3],
4+
"nested": { "empty": {} },
5+
"id": 9007199254740993,
6+
"precise": 1.50,
7+
"huge": 1e400,
8+
"text": "keep \"these\" spaces\\"
9+
}

0 commit comments

Comments
 (0)