Skip to content

Commit 0106204

Browse files
authored
fix: minify inject CSS in prod mode (#14006)
When CSS is externalized we rightfully rely on the following tooling chain to properly minify CSS. When we inject the CSS however, that tooling won't be able to do that, so we gotta do it ourselves. This PR brings back most of that logic that existed in Svelte 4. Fixes #13716
1 parent e487f61 commit 0106204

File tree

7 files changed

+90
-33
lines changed

7 files changed

+90
-33
lines changed

.changeset/red-berries-watch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: minify inject CSS in prod mode

packages/svelte/src/compiler/phases/3-transform/css/index.js

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { dev } from '../../../state.js';
1111
* @typedef {{
1212
* code: MagicString;
1313
* hash: string;
14+
* minify: boolean;
1415
* selector: string;
1516
* keyframes: string[];
1617
* specificity: {
@@ -32,6 +33,7 @@ export function render_stylesheet(source, analysis, options) {
3233
const state = {
3334
code,
3435
hash: analysis.css.hash,
36+
minify: analysis.inject_styles && !options.dev,
3537
selector: `.${analysis.css.hash}`,
3638
keyframes: analysis.css.keyframes,
3739
specificity: {
@@ -45,6 +47,9 @@ export function render_stylesheet(source, analysis, options) {
4547

4648
code.remove(0, ast.content.start);
4749
code.remove(/** @type {number} */ (ast.content.end), source.length);
50+
if (state.minify) {
51+
remove_preceeding_whitespace(ast.content.end, state);
52+
}
4853

4954
const css = {
5055
code: code.toString(),
@@ -116,22 +121,47 @@ const visitors = {
116121

117122
index++;
118123
}
124+
} else if (state.minify) {
125+
remove_preceeding_whitespace(node.start, state);
126+
127+
// Don't minify whitespace in custom properties, since some browsers (Chromium < 99)
128+
// treat --foo: ; and --foo:; differently
129+
if (!node.property.startsWith('--')) {
130+
let start = node.start + node.property.length + 1;
131+
let end = start;
132+
while (/\s/.test(state.code.original[end])) end++;
133+
if (end > start) state.code.remove(start, end);
134+
}
119135
}
120136
},
121137
Rule(node, { state, next, visit }) {
138+
if (state.minify) {
139+
remove_preceeding_whitespace(node.start, state);
140+
remove_preceeding_whitespace(node.block.end - 1, state);
141+
}
142+
122143
// keep empty rules in dev, because it's convenient to
123144
// see them in devtools
124145
if (!dev && is_empty(node)) {
125-
state.code.prependRight(node.start, '/* (empty) ');
126-
state.code.appendLeft(node.end, '*/');
127-
escape_comment_close(node, state.code);
146+
if (state.minify) {
147+
state.code.remove(node.start, node.end);
148+
} else {
149+
state.code.prependRight(node.start, '/* (empty) ');
150+
state.code.appendLeft(node.end, '*/');
151+
escape_comment_close(node, state.code);
152+
}
153+
128154
return;
129155
}
130156

131157
if (!is_used(node)) {
132-
state.code.prependRight(node.start, '/* (unused) ');
133-
state.code.appendLeft(node.end, '*/');
134-
escape_comment_close(node, state.code);
158+
if (state.minify) {
159+
state.code.remove(node.start, node.end);
160+
} else {
161+
state.code.prependRight(node.start, '/* (unused) ');
162+
state.code.appendLeft(node.end, '*/');
163+
escape_comment_close(node, state.code);
164+
}
135165

136166
return;
137167
}
@@ -141,11 +171,16 @@ const visitors = {
141171

142172
if (selector.children.length === 1 && selector.children[0].selectors.length === 1) {
143173
// `:global {...}`
144-
state.code.prependRight(node.start, '/* ');
145-
state.code.appendLeft(node.block.start + 1, '*/');
174+
if (state.minify) {
175+
state.code.remove(node.start, node.block.start + 1);
176+
state.code.remove(node.block.end - 1, node.end);
177+
} else {
178+
state.code.prependRight(node.start, '/* ');
179+
state.code.appendLeft(node.block.start + 1, '*/');
146180

147-
state.code.prependRight(node.block.end - 1, '/*');
148-
state.code.appendLeft(node.block.end, '*/');
181+
state.code.prependRight(node.block.end - 1, '/*');
182+
state.code.appendLeft(node.block.end, '*/');
183+
}
149184

150185
// don't recurse into selector or body
151186
return;
@@ -162,7 +197,8 @@ const visitors = {
162197
// Only add comments if we're not inside a complex selector that itself is unused
163198
if (!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)) {
164199
let pruning = false;
165-
let last = node.children[0].start;
200+
let prune_start = node.children[0].start;
201+
let last = prune_start;
166202

167203
for (let i = 0; i < node.children.length; i += 1) {
168204
const selector = node.children[i];
@@ -172,12 +208,20 @@ const visitors = {
172208
let i = selector.start;
173209
while (state.code.original[i] !== ',') i--;
174210

175-
state.code.overwrite(i, i + 1, '*/');
176-
} else {
177-
if (i === 0) {
178-
state.code.prependRight(selector.start, '/* (unused) ');
211+
if (state.minify) {
212+
state.code.remove(prune_start, i + 1);
179213
} else {
180-
state.code.overwrite(last, selector.start, ' /* (unused) ');
214+
state.code.overwrite(i, i + 1, '*/');
215+
}
216+
} else {
217+
prune_start = selector.start;
218+
219+
if (!state.minify) {
220+
if (i === 0) {
221+
state.code.prependRight(selector.start, '/* (unused) ');
222+
} else {
223+
state.code.overwrite(last, selector.start, ' /* (unused) ');
224+
}
181225
}
182226
}
183227

@@ -188,7 +232,11 @@ const visitors = {
188232
}
189233

190234
if (pruning) {
191-
state.code.appendLeft(last, '*/');
235+
if (state.minify) {
236+
state.code.remove(prune_start, last);
237+
} else {
238+
state.code.appendLeft(last, '*/');
239+
}
192240
}
193241
}
194242

@@ -320,6 +368,17 @@ const visitors = {
320368
}
321369
};
322370

371+
/**
372+
* Walk backwards until we find a non-whitespace character
373+
* @param {number} end
374+
* @param {State} state
375+
*/
376+
function remove_preceeding_whitespace(end, state) {
377+
let start = end;
378+
while (/\s/.test(state.code.original[start - 1])) start--;
379+
if (start < end) state.code.remove(start, end);
380+
}
381+
323382
/** @param {Css.Rule} rule */
324383
function is_empty(rule) {
325384
if (rule.metadata.is_global_block) {

packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/Nested.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@
66
.bar {
77
color: red;
88
}
9+
.unused {
10+
.also-unused {
11+
color: green;
12+
}
13+
}
914
</style>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<!--[--><div class="bar svelte-ievf05">bar</div><!----> <div class="foo svelte-sg04hs">foo</div><!--]-->
1+
<!--[--><div class="bar svelte-1fs6vx">bar</div><!----> <div class="foo svelte-sg04hs">foo</div><!--]-->
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
<style id="svelte-ievf05">
2-
.bar.svelte-ievf05 {
3-
color: red;
4-
}
5-
</style>
1+
<style id="svelte-1fs6vx">.bar.svelte-1fs6vx {color:red;}</style>
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
<style id="svelte-sg04hs">
2-
.foo.svelte-sg04hs {
3-
color: red;
4-
}
5-
</style>
1+
<style id="svelte-sg04hs">.foo.svelte-sg04hs {color:red;}</style>
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
<style id="svelte-sg04hs">
2-
.foo.svelte-sg04hs {
3-
color: red;
4-
}
5-
</style>
1+
<style id="svelte-sg04hs">.foo.svelte-sg04hs {color:red;}</style>

0 commit comments

Comments
 (0)