Skip to content

Commit 0117109

Browse files
feat: add css.hasGlobal to compile output (#15450)
* feat: add `hasUnscopedGlobalCss` to `compile` metadata * chore: rename to `has_unscoped_global` * fix: handle `-global` keyframes * chore: guard the check if the value is already true * update types * add tests * tweak * tweak * regenerate types * Update .changeset/plenty-hotels-mix.md * fix test, add failing test * fix * fix * fix jsdoc * unused * fix * lint * rename * rename * reduce indirection * tidy up * revert * tweak * lint --------- Co-authored-by: Rich Harris <[email protected]>
1 parent ec1d85c commit 0117109

File tree

20 files changed

+155
-21
lines changed

20 files changed

+155
-21
lines changed

.changeset/plenty-hotels-mix.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: add `css.hasGlobal` to `compile` output

packages/svelte/src/compiler/phases/1-parse/read/style.js

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ function read_rule(parser) {
118118
metadata: {
119119
parent_rule: null,
120120
has_local_selectors: false,
121+
has_global_selectors: false,
121122
is_global_block: false
122123
}
123124
};
@@ -342,6 +343,7 @@ function read_selector(parser, inside_pseudo_class = false) {
342343
children,
343344
metadata: {
344345
rule: null,
346+
is_global: false,
345347
used: false
346348
}
347349
};

packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js

+52-18
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import { is_keyframes_node } from '../../css.js';
77
import { is_global, is_unscoped_pseudo_class } from './utils.js';
88

99
/**
10-
* @typedef {Visitors<
11-
* AST.CSS.Node,
12-
* {
13-
* keyframes: string[];
14-
* rule: AST.CSS.Rule | null;
15-
* }
16-
* >} CssVisitors
10+
* @typedef {{
11+
* keyframes: string[];
12+
* rule: AST.CSS.Rule | null;
13+
* analysis: ComponentAnalysis;
14+
* }} CssState
15+
*/
16+
17+
/**
18+
* @typedef {Visitors<AST.CSS.Node, CssState>} CssVisitors
1719
*/
1820

1921
/**
@@ -28,6 +30,15 @@ function is_global_block_selector(simple_selector) {
2830
);
2931
}
3032

33+
/**
34+
* @param {AST.SvelteNode[]} path
35+
*/
36+
function is_unscoped(path) {
37+
return path
38+
.filter((node) => node.type === 'Rule')
39+
.every((node) => node.metadata.has_global_selectors);
40+
}
41+
3142
/**
3243
*
3344
* @param {Array<AST.CSS.Node>} path
@@ -42,6 +53,9 @@ const css_visitors = {
4253
if (is_keyframes_node(node)) {
4354
if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) {
4455
context.state.keyframes.push(node.prelude);
56+
} else if (node.prelude.startsWith('-global-')) {
57+
// we don't check if the block.children.length because the keyframe is still added even if empty
58+
context.state.analysis.css.has_global ||= is_unscoped(context.path);
4559
}
4660
}
4761

@@ -99,10 +113,12 @@ const css_visitors = {
99113

100114
node.metadata.rule = context.state.rule;
101115

102-
node.metadata.used ||= node.children.every(
116+
node.metadata.is_global = node.children.every(
103117
({ metadata }) => metadata.is_global || metadata.is_global_like
104118
);
105119

120+
node.metadata.used ||= node.metadata.is_global;
121+
106122
if (
107123
node.metadata.rule?.metadata.parent_rule &&
108124
node.children[0]?.selectors[0]?.type === 'NestingSelector'
@@ -190,6 +206,7 @@ const css_visitors = {
190206

191207
if (idx !== -1) {
192208
is_global_block = true;
209+
193210
for (let i = idx + 1; i < child.selectors.length; i++) {
194211
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
195212
ComplexSelector(node) {
@@ -242,16 +259,26 @@ const css_visitors = {
242259
}
243260
}
244261

245-
context.next({
246-
...context.state,
247-
rule: node
248-
});
262+
const state = { ...context.state, rule: node };
249263

250-
node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
251-
return selector.children.some(
252-
({ metadata }) => !metadata.is_global && !metadata.is_global_like
253-
);
254-
});
264+
// visit selector list first, to populate child selector metadata
265+
context.visit(node.prelude, state);
266+
267+
for (const selector of node.prelude.children) {
268+
node.metadata.has_global_selectors ||= selector.metadata.is_global;
269+
node.metadata.has_local_selectors ||= !selector.metadata.is_global;
270+
}
271+
272+
// if this rule has a ComplexSelector whose RelativeSelector children are all
273+
// `:global(...)`, and the rule contains declarations (rather than just
274+
// nested rules) then the component as a whole includes global CSS
275+
context.state.analysis.css.has_global ||=
276+
node.metadata.has_global_selectors &&
277+
node.block.children.filter((child) => child.type === 'Declaration').length > 0 &&
278+
is_unscoped(context.path);
279+
280+
// visit block list, so parent rule metadata is populated
281+
context.visit(node.block, state);
255282
},
256283
NestingSelector(node, context) {
257284
const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
@@ -289,5 +316,12 @@ const css_visitors = {
289316
* @param {ComponentAnalysis} analysis
290317
*/
291318
export function analyze_css(stylesheet, analysis) {
292-
walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
319+
/** @type {CssState} */
320+
const css_state = {
321+
keyframes: analysis.css.keyframes,
322+
rule: null,
323+
analysis
324+
};
325+
326+
walk(stylesheet, css_state, css_visitors);
293327
}

packages/svelte/src/compiler/phases/2-analyze/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,8 @@ export function analyze_component(root, source, options) {
456456
hash
457457
})
458458
: '',
459-
keyframes: []
459+
keyframes: [],
460+
has_global: false
460461
},
461462
source,
462463
undefined_exports: new Map(),

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ export function render_stylesheet(source, analysis, options) {
5959
// generateMap takes care of calculating source relative to file
6060
source: options.filename,
6161
file: options.cssOutputFilename || options.filename
62-
})
62+
}),
63+
hasGlobal: analysis.css.has_global
6364
};
6465

6566
merge_with_preprocessor_map(css, options, css.map.sources[0]);

packages/svelte/src/compiler/phases/types.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export interface ComponentAnalysis extends Analysis {
7474
ast: AST.CSS.StyleSheet | null;
7575
hash: string;
7676
keyframes: string[];
77+
has_global: boolean;
7778
};
7879
source: string;
7980
undefined_exports: Map<string, Node>;

packages/svelte/src/compiler/types/css.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export namespace _CSS {
3434
metadata: {
3535
parent_rule: null | Rule;
3636
has_local_selectors: boolean;
37+
/**
38+
* `true` if the rule contains a ComplexSelector whose RelativeSelectors are all global or global-like
39+
*/
40+
has_global_selectors: boolean;
3741
/**
3842
* `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped
3943
*/
@@ -64,6 +68,7 @@ export namespace _CSS {
6468
/** @internal */
6569
metadata: {
6670
rule: null | Rule;
71+
is_global: boolean;
6772
/** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */
6873
used: boolean;
6974
};

packages/svelte/src/compiler/types/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface CompileResult {
1818
code: string;
1919
/** A source map */
2020
map: SourceMap;
21+
/** Whether or not the CSS includes global rules */
22+
hasGlobal: boolean;
2123
};
2224
/**
2325
* An array of warning objects that were generated during compilation. Each warning has several properties:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
hasGlobal: true
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
hasGlobal: false
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
div.svelte-xyz {
3+
.whatever {
4+
color: green;
5+
}
6+
}
7+
8+
.whatever {
9+
div.svelte-xyz {
10+
color: green;
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<div>{@html whatever}</div>
2+
3+
<style>
4+
div {
5+
:global(.whatever) {
6+
color: green;
7+
}
8+
}
9+
10+
:global(.whatever) {
11+
div {
12+
color: green;
13+
}
14+
}
15+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
hasGlobal: false
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
div.svelte-xyz .whatever {
3+
color: green;
4+
}
5+
6+
.whatever div.svelte-xyz {
7+
color: green;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div>{@html whatever}</div>
2+
3+
<style>
4+
div :global(.whatever) {
5+
color: green;
6+
}
7+
8+
:global(.whatever) div {
9+
color: green;
10+
}
11+
</style>
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { test } from '../../test';
22

33
export default test({
4-
warnings: []
4+
warnings: [],
5+
6+
hasGlobal: false
57
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
hasGlobal: true
5+
});

packages/svelte/tests/css/test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface CssTest extends BaseTest {
3434
compileOptions?: Partial<CompileOptions>;
3535
warnings?: Warning[];
3636
props?: Record<string, any>;
37+
hasGlobal?: boolean;
3738
}
3839

3940
/**
@@ -78,6 +79,14 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
7879
// assert_html_equal(actual_ssr, expected.html);
7980
}
8081

82+
if (config.hasGlobal !== undefined) {
83+
const metadata = JSON.parse(
84+
fs.readFileSync(`${cwd}/_output/client/input.svelte.css.json`, 'utf-8')
85+
);
86+
87+
assert.equal(metadata.hasGlobal, config.hasGlobal);
88+
}
89+
8190
const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim();
8291
const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim();
8392

packages/svelte/tests/helpers.js

+4
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ export async function compile_directory(
146146

147147
if (compiled.css) {
148148
write(`${output_dir}/${file}.css`, compiled.css.code);
149+
write(
150+
`${output_dir}/${file}.css.json`,
151+
JSON.stringify({ hasGlobal: compiled.css.hasGlobal })
152+
);
149153
if (output_map) {
150154
write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t'));
151155
}

packages/svelte/types/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,8 @@ declare module 'svelte/compiler' {
753753
code: string;
754754
/** A source map */
755755
map: SourceMap;
756+
/** Whether or not the CSS includes global rules */
757+
hasGlobal: boolean;
756758
};
757759
/**
758760
* An array of warning objects that were generated during compilation. Each warning has several properties:

0 commit comments

Comments
 (0)