Skip to content

Commit 021b382

Browse files
fix: read a rule's contents past a quoted brace, and keep the caller's options
`ruleBody` now skips braces inside strings and comments instead of declining the rule, `esbuildMinifyCss` no longer deletes `as` from the options object it is handed, and `swcMinifyCss` drops a map the options asked for when the body was wrapped. Claude-Session: https://claude.ai/code/session_01Vszf1WJhCqatsMQ2xZLF9x
1 parent 1790f6f commit 021b382

2 files changed

Lines changed: 110 additions & 12 deletions

File tree

‎src/utils.js‎

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,37 @@ function ruleBody(answered) {
8686

8787
if (written === "") return "";
8888

89-
const match = /^a\s*\{([^{}]*)\}$/.exec(written);
89+
const opened = /^a\s*\{/.exec(written);
9090

91-
return match ? match[1].trim() : undefined;
91+
if (!opened || !written.endsWith("}")) return undefined;
92+
93+
const end = written.length - 1;
94+
95+
// A brace a string or a comment holds is text rather than a block's edge, and
96+
// one left open would reach past the brace closing the rule.
97+
for (let i = opened[0].length; i < end; i++) {
98+
const char = written[i];
99+
100+
if (char === "{" || char === "}") return undefined;
101+
102+
if (char === "\\") {
103+
i++;
104+
} else if (char === '"' || char === "'") {
105+
for (i++; i < end && written[i] !== char; i++) {
106+
if (written[i] === "\\") i++;
107+
}
108+
109+
if (i >= end) return undefined;
110+
} else if (char === "/" && written[i + 1] === "*") {
111+
const closed = written.indexOf("*/", i + 2);
112+
113+
if (closed === -1 || closed + 2 > end) return undefined;
114+
115+
i = closed + 1;
116+
}
117+
}
118+
119+
return written.slice(opened[0].length, end).trim();
92120
}
93121

94122
/**
@@ -1878,23 +1906,23 @@ async function esbuildMinifyCss(input, sourceMap, minimizerOptions) {
18781906
* @param {import("esbuild").TransformOptions & { ecma?: string | number, module?: boolean, as?: string }=} esbuildOptions esbuild options
18791907
* @returns {import("esbuild").TransformOptions} built esbuild options
18801908
*/
1881-
const buildEsbuildOptions = (esbuildOptions = {}) => {
1909+
const buildEsbuildOptions = ({
18821910
// `module` and `ecma` are JavaScript-only concepts; the dispatcher
18831911
// injects them for every minimizer, and `as` is the body's rather than
18841912
// esbuild's, but esbuild's CSS transform rejects unknown options.
1885-
delete esbuildOptions.ecma;
1886-
delete esbuildOptions.module;
1887-
delete esbuildOptions.as;
1888-
1913+
ecma,
1914+
module,
1915+
as,
1916+
...esbuildOptions
1917+
} = {}) =>
18891918
// Need deep copy objects to avoid https://github.com/terser/terser/issues/366
1890-
return {
1919+
({
18911920
loader: "css",
18921921
minify: true,
18931922
legalComments: "inline",
18941923
...esbuildOptions,
18951924
sourcemap: false,
1896-
};
1897-
};
1925+
});
18981926

18991927
let esbuild;
19001928

@@ -2148,8 +2176,11 @@ async function swcMinifyCss(input, sourceMap, minimizerOptions) {
21482176
const swcOptions = buildSwcOptions(minimizerOptions);
21492177

21502178
// Let `swc` generate a SourceMap; a wrap moves every position, so the map
2151-
// would describe a stylesheet that is not what comes back.
2152-
if (sourceMap && !contents) {
2179+
// would describe a stylesheet that is not what comes back, whatever the
2180+
// options asked for.
2181+
if (contents) {
2182+
swcOptions.sourceMap = false;
2183+
} else if (sourceMap) {
21532184
swcOptions.sourceMap = true;
21542185
}
21552186

‎test/embedded-protocol.test.js‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,11 +893,78 @@ describe("the rule a block's contents are minified inside", () => {
893893
expect(ruleBody("")).toBe("");
894894
});
895895

896+
it("reads past a brace a string or a comment holds", () => {
897+
expect(ruleBody('a{content:"{"}')).toBe('content:"{"');
898+
expect(ruleBody("a{content:'}'}")).toBe("content:'}'");
899+
expect(ruleBody('a{content:"\\"}"}')).toBe('content:"\\"}"');
900+
expect(ruleBody("a{/* } */color:red}")).toBe("/* } */color:red");
901+
});
902+
896903
it("declines an answer that is not that one rule", () => {
897904
expect(ruleBody("color:red")).toBeUndefined();
898905
expect(ruleBody("a{color:red}b{color:blue}")).toBeUndefined();
899906
expect(ruleBody("b{color:red}")).toBeUndefined();
900907
expect(ruleBody("@media print{a{color:red}}")).toBeUndefined();
901908
expect(ruleBody(undefined)).toBeUndefined();
909+
expect(ruleBody('a{content:"{"}b{color:blue}')).toBeUndefined();
910+
expect(ruleBody("a{color:red")).toBeUndefined();
911+
});
912+
});
913+
914+
describe("a CSS minimizer handed a block's contents directly", () => {
915+
it("minifies a string holding a brace", async () => {
916+
const result = await MinimizerPlugin.cssoMinify(
917+
{ "style.css": ' content : "{" ' },
918+
undefined,
919+
{ as: "block-contents" },
920+
);
921+
922+
expect(result.code).toBe('content:"{"');
923+
});
924+
925+
it("leaves the options it was handed as they were", async () => {
926+
const options = { as: "block-contents" };
927+
const input = { "style.css": " color : red " };
928+
929+
const first = await MinimizerPlugin.esbuildMinifyCss(
930+
input,
931+
undefined,
932+
options,
933+
);
934+
const second = await MinimizerPlugin.esbuildMinifyCss(
935+
input,
936+
undefined,
937+
options,
938+
);
939+
940+
expect(options).toEqual({ as: "block-contents" });
941+
expect(first.code).toBe("color:red");
942+
expect(second.code).toBe("color:red");
902943
});
944+
945+
it.each([
946+
["cssoMinify", MinimizerPlugin.cssoMinify, { sourceMap: true }],
947+
["cleanCssMinify", MinimizerPlugin.cleanCssMinify, { sourceMap: true }],
948+
["esbuildMinifyCss", MinimizerPlugin.esbuildMinifyCss, { sourcemap: true }],
949+
[
950+
"lightningCssMinify",
951+
MinimizerPlugin.lightningCssMinify,
952+
{ sourceMap: true },
953+
],
954+
["swcMinifyCss", MinimizerPlugin.swcMinifyCss, { sourceMap: true }],
955+
])(
956+
"returns no map for the rule `%s` minified it inside",
957+
async (name, minifier, mapOptions) => {
958+
// The wrap moves every position, so a map asked for by the options too
959+
// would describe a stylesheet that is not what comes back.
960+
const result = await minifier(
961+
{ "style.css": " color : red " },
962+
{ version: 3, sources: [], names: [], mappings: "" },
963+
{ as: "block-contents", ...mapOptions },
964+
);
965+
966+
expect(result.code).toBe("color:red");
967+
expect(result.map).toBeUndefined();
968+
},
969+
);
903970
});

0 commit comments

Comments
 (0)