Skip to content

Commit 09ebfa2

Browse files
committed
feat(html): trim the body of a script holding a data block
A `type` that is neither a JavaScript MIME nor JSON makes the element a data block — a template, a raw payload — which nothing executes or parses. Its own syntax stays as written, `{{ }}` and the spacing inside it included, but the whitespace around it reaches no consumer, so collapsing takes it as @swc/html does.
1 parent 1f3264e commit 09ebfa2

9 files changed

Lines changed: 174 additions & 2 deletions

File tree

‎.changeset/010-css-html-minify.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"webpack": patch
33
---
44

5-
Minify more HTML and CSS shorthands, and cut two costs off printing.
5+
Minify more HTML and CSS shorthands, trim the body of a <script> holding a data block, and cut two costs off printing.

‎lib/html/syntax.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11914,16 +11914,22 @@ const printer = (path, writer) => {
1191411914
if (JSON_SCRIPT_TYPES.has(type) || _JSON_SUBTYPE_REGEXP.test(type)) {
1191511915
return _minifyInlineJson(data);
1191611916
}
11917+
const executable = JAVASCRIPT_SCRIPT_TYPES.has(type);
1191711918
// No built-in: webpack ships no JS minifier, so an inline script is
1191811919
// touched only by a caller's renderer.
1191911920
if (
11921+
executable &&
1192011922
(_renderEmbeddedSource !== undefined ||
1192111923
_deferEmbeddedSource !== undefined) &&
11922-
JAVASCRIPT_SCRIPT_TYPES.has(type) &&
1192311924
data.trim() !== ""
1192411925
) {
1192511926
return _renderEmbedded(data, JAVASCRIPT_TYPE);
1192611927
}
11928+
// A data block — a template, a plain-text payload. Its own syntax is
11929+
// not ours to read, but nothing consumes the whitespace around it.
11930+
if (!executable && _collapseWhitespace !== "none") {
11931+
return _asciiTrim(data);
11932+
}
1192711933
}
1192811934
return data;
1192911935
}

‎test/HtmlSyntax.unittest.js‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4141,6 +4141,91 @@ describe("SourceProcessor — JSON <script> bodies", () => {
41414141
expect(minifyBody("text/template", '{ "a" : 1 }')).toBe('{ "a" : 1 }');
41424142
});
41434143

4144+
/**
4145+
* @param {string} type the `<script type>` value
4146+
* @param {string} body the raw body
4147+
* @returns {string} the body, collapsing whitespace
4148+
*/
4149+
const collapseBody = (type, body) => {
4150+
const out = new SourceProcessor().process(
4151+
`<script type="${type}">${body}</script>`,
4152+
{ mode: "minify", collapseWhitespace: "all" }
4153+
).code;
4154+
return out.slice(out.indexOf(">") + 1, out.lastIndexOf("</script>"));
4155+
};
4156+
4157+
it.each([
4158+
"text/template",
4159+
"text/x-template",
4160+
"text/ng-template",
4161+
"text/x-handlebars-template",
4162+
"text/html",
4163+
"unknown/thing"
4164+
])("trims the body of the data block %s", (type) => {
4165+
expect(collapseBody(type, " <div> x </div> ")).toBe("<div> x </div>");
4166+
});
4167+
4168+
it("only trims — a data block's own syntax is not read", () => {
4169+
// The template language owns what is inside, `{{ }}` and its spacing too.
4170+
expect(
4171+
collapseBody("text/x-template", " <p class='a'> {{ t }} </p> ")
4172+
).toBe("<p class='a'> {{ t }} </p>");
4173+
});
4174+
4175+
it.each(["", "module", "text/javascript"])(
4176+
"leaves an executable body alone (%s)",
4177+
(type) => {
4178+
const body = " var a = 1 ";
4179+
expect(collapseBody(type, body)).toBe(body);
4180+
}
4181+
);
4182+
4183+
it("reads a data block's type case- and whitespace-insensitively", () => {
4184+
expect(collapseBody(" TEXT/X-TEMPLATE ", " x ")).toBe("x");
4185+
});
4186+
4187+
it("takes every character HTML counts as whitespace", () => {
4188+
expect(collapseBody("text/x-template", "\t\n\f\r x \r\f\n\t")).toBe("x");
4189+
expect(collapseBody("text/x-template", " ")).toBe("");
4190+
});
4191+
4192+
it("keeps a NBSP, which is a character and not whitespace", () => {
4193+
// `@swc/html` trims it, which edits what the body renders.
4194+
expect(collapseBody("text/x-template", "\u00A0\u00A0x\u00A0\u00A0")).toBe(
4195+
"\u00A0\u00A0x\u00A0\u00A0"
4196+
);
4197+
});
4198+
4199+
it.each(
4200+
/** @type {("conservative" | "smart" | "all")[]} */ ([
4201+
"conservative",
4202+
"smart",
4203+
"all"
4204+
])
4205+
)("trims where collapsing is %s", (collapseWhitespace) => {
4206+
const out = new SourceProcessor().process(
4207+
'<script type="text/x-template"> x </script>',
4208+
{ mode: "minify", collapseWhitespace }
4209+
).code;
4210+
expect(out).toContain(">x<");
4211+
});
4212+
4213+
it("trims a data block in foreign content too", () => {
4214+
const out = new SourceProcessor().process(
4215+
'<svg><script type="text/x-template"> x </script></svg>',
4216+
{ mode: "minify", collapseWhitespace: "all" }
4217+
).code;
4218+
expect(out).toContain(">x<");
4219+
});
4220+
4221+
it("leaves a data block alone where whitespace is kept", () => {
4222+
const out = new SourceProcessor().process(
4223+
'<script type="text/x-template"> <div> x </div> </script>',
4224+
{ mode: "minify" }
4225+
).code;
4226+
expect(out).toContain("> <div> x </div> <");
4227+
});
4228+
41444229
it("copies every literal byte for byte", () => {
41454230
// Re-serializing would round the number through a double, drop the
41464231
// duplicate key and rewrite the escape.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`ConfigCacheTestCases html minimize-script-data-blocks minimize-script-data-blocks should compile 1`] = `"<!doctype html><html lang=en><head><title>Data blocks</title><script type=application/ld+json>{\\"a\\":1}</script></head><body> <script type=text/x-template><p class=\\"a\\"> {{ t }} </p></script> <script type=text/html><div> x </div></script> <script type=unknown/thing>raw payload</script> <script src=__html_cbe5b59d_0.js></script> </body></html>"`;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`ConfigTestCases html minimize-script-data-blocks minimize-script-data-blocks should compile 1`] = `"<!doctype html><html lang=en><head><title>Data blocks</title><script type=application/ld+json>{\\"a\\":1}</script></head><body> <script type=text/x-template><p class=\\"a\\"> {{ t }} </p></script> <script type=text/html><div> x </div></script> <script type=unknown/thing>raw payload</script> <script src=__html_cbe5b59d_0.js></script> </body></html>"`;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import "./page.html";
2+
3+
it("should trim a data block's body but not read its syntax", () => {
4+
// The emitted file is the assertion — see the snapshot in test.config.js.
5+
expect(true).toBe(true);
6+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>Data blocks</title>
5+
<script type="application/ld+json"> { "a" : 1 } </script>
6+
</head>
7+
<body>
8+
<script type="text/x-template"> <p class="a"> {{ t }} </p> </script>
9+
<script type="text/html"> <div> x </div> </script>
10+
<script type="unknown/thing"> raw payload </script>
11+
<script> var a = 1 </script>
12+
</body>
13+
</html>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use strict";
2+
3+
const fs = require("fs");
4+
const path = require("path");
5+
6+
module.exports = {
7+
findBundle(_i, options) {
8+
const files = fs.readdirSync(options.output.path);
9+
return files.includes("main.js") ? ["./main.js"] : undefined;
10+
},
11+
afterExecute(options) {
12+
const page = fs.readFileSync(
13+
path.join(options.output.path, "page.html"),
14+
"utf8"
15+
);
16+
expect(page).toMatchSnapshot();
17+
// The template language owns what is inside; only the edges go.
18+
expect(page).toContain('<p class="a"> {{ t }} </p>');
19+
}
20+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use strict";
2+
3+
/** @type {import("../../../../").Configuration} */
4+
module.exports = {
5+
target: "web",
6+
mode: "production",
7+
output: {
8+
filename: "[name].js",
9+
pathinfo: false
10+
},
11+
module: {
12+
generator: {
13+
html: {
14+
extract: true
15+
}
16+
},
17+
parser: {
18+
html: {
19+
sources: false
20+
}
21+
}
22+
},
23+
optimization: {
24+
// `"..."` keeps the default minimizer, which hands
25+
// `optimization.minimize.html` to `htmlMinify`.
26+
minimize: {
27+
html: {
28+
collapseWhitespace: true
29+
}
30+
},
31+
minimizer: ["..."]
32+
},
33+
experiments: {
34+
html: true
35+
}
36+
};

0 commit comments

Comments
 (0)