From e714b895455569073f46cee2f5f11d25f2b9ca35 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 5 Mar 2025 16:02:58 +0100 Subject: [PATCH 1/4] feat: add `hasUnscopedGlobalCss` to `compile` metadata --- .changeset/plenty-hotels-mix.md | 5 ++ .../phases/2-analyze/css/css-analyze.js | 53 ++++++++++++++++++- .../src/compiler/phases/2-analyze/index.js | 3 +- .../src/compiler/phases/3-transform/index.js | 12 +++-- .../svelte/src/compiler/phases/types.d.ts | 1 + packages/svelte/src/compiler/types/index.d.ts | 5 ++ packages/svelte/types/index.d.ts | 5 ++ 7 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 .changeset/plenty-hotels-mix.md diff --git a/.changeset/plenty-hotels-mix.md b/.changeset/plenty-hotels-mix.md new file mode 100644 index 000000000000..226872f99c02 --- /dev/null +++ b/.changeset/plenty-hotels-mix.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `hasUnscopedGlobalCss` to `compile` metadata diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index ed228385820a..0b83053cef5f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -7,11 +7,13 @@ import { is_keyframes_node } from '../../css.js'; import { is_global, is_unscoped_pseudo_class } from './utils.js'; /** + * We need to use an object for `has_global_unscoped` since state is spread * @typedef {Visitors< * AST.CSS.Node, * { * keyframes: string[]; * rule: AST.CSS.Rule | null; + * has_global_unscoped: { value: boolean }; * } * >} CssVisitors */ @@ -28,6 +30,30 @@ function is_global_block_selector(simple_selector) { ); } +/** + * @param {import('../types.js').Context["path"]} path + * @param {AST.CSS.Rule | null} [rule] + * @returns + */ +function is_unscoped_global(path, rule) { + // remove every at rule or stylesheet and the current rule in case is passed in from `ComplexSelector` + const parents = path.filter( + (parent) => parent.type !== 'Atrule' && parent.type !== 'StyleSheet' && parent !== rule + ); + + let unscoped_global = true; + + // no parents means we are on top + if (parents.length > 0) { + // let's check that everything in the path is not a global block + unscoped_global = parents.every((parent) => { + return parent.type !== 'Rule' || parent.metadata.is_global_block; + }); + } + + return unscoped_global; +} + /** * * @param {Array} path @@ -64,6 +90,16 @@ const css_visitors = { } } } + + if (idx === 0) { + if ( + is_unscoped_global(context.path, context.state.rule) && + context.state.rule && + context.state.rule.block.children.length > 0 + ) { + context.state.has_global_unscoped.value = true; + } + } } } @@ -174,7 +210,8 @@ const css_visitors = { node.metadata.is_global_block = node.prelude.children.some((selector) => { let is_global_block = false; - for (const child of selector.children) { + for (let i = 0; i < selector.children.length; i++) { + const child = selector.children[i]; const idx = child.selectors.findIndex(is_global_block_selector); if (is_global_block) { @@ -184,6 +221,11 @@ const css_visitors = { if (idx !== -1) { is_global_block = true; + if (i === 0) { + if (is_unscoped_global(context.path) && node.block.children.length > 0) { + context.state.has_global_unscoped.value = true; + } + } for (let i = idx + 1; i < child.selectors.length; i++) { walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { ComplexSelector(node) { @@ -283,5 +325,12 @@ const css_visitors = { * @param {ComponentAnalysis} analysis */ export function analyze_css(stylesheet, analysis) { - walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors); + const css_state = { + keyframes: analysis.css.keyframes, + rule: null, + // we need to use an object since state is spread + has_global_unscoped: { value: false } + }; + walk(stylesheet, css_state, css_visitors); + analysis.css.has_global_unscoped = css_state.has_global_unscoped.value; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 322293bf6b91..8e6cc8fd134f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -455,7 +455,8 @@ export function analyze_component(root, source, options) { hash }) : '', - keyframes: [] + keyframes: [], + has_global_unscoped: false }, source, undefined_exports: new Map(), diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index f96fd64ec7a9..9e02675feaed 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -21,7 +21,8 @@ export function transform_component(analysis, source, options) { css: null, warnings: state.warnings, // set afterwards metadata: { - runes: analysis.runes + runes: analysis.runes, + hasUnscopedGlobalCss: analysis.css.has_global_unscoped }, ast: /** @type {any} */ (null) // set afterwards }; @@ -53,7 +54,8 @@ export function transform_component(analysis, source, options) { css, warnings: state.warnings, // set afterwards. TODO apply preprocessor sourcemap metadata: { - runes: analysis.runes + runes: analysis.runes, + hasUnscopedGlobalCss: analysis.css.has_global_unscoped }, ast: /** @type {any} */ (null) // set afterwards }; @@ -72,7 +74,8 @@ export function transform_module(analysis, source, options) { css: null, warnings: state.warnings, // set afterwards metadata: { - runes: true + runes: true, + hasUnscopedGlobalCss: false }, ast: /** @type {any} */ (null) // set afterwards }; @@ -102,7 +105,8 @@ export function transform_module(analysis, source, options) { }), css: null, metadata: { - runes: true + runes: true, + hasUnscopedGlobalCss: false }, warnings: state.warnings, // set afterwards ast: /** @type {any} */ (null) // set afterwards diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index abe2b115de02..f5b480cb317d 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -73,6 +73,7 @@ export interface ComponentAnalysis extends Analysis { ast: AST.CSS.StyleSheet | null; hash: string; keyframes: string[]; + has_global_unscoped: boolean; }; source: string; undefined_exports: Map; diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index eec41bad9d25..7ee97954a5a4 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -35,6 +35,11 @@ export interface CompileResult { * For `compileModule`, this is always `true` */ runes: boolean; + /** + * Whether the component contains a top level :global selector or not + * For `compileModule`, this is always `true` + */ + hasUnscopedGlobalCss: boolean; }; /** The AST */ ast: any; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c3dbdcac791e..03ed49050c7a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -769,6 +769,11 @@ declare module 'svelte/compiler' { * For `compileModule`, this is always `true` */ runes: boolean; + /** + * Whether the component contains a top level :global selector or not + * For `compileModule`, this is always `true` + */ + hasUnscopedGlobalCss: boolean; }; /** The AST */ ast: any; From 89682a21b8f3371d28ea1bcfad1b58028a5fb5ac Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 5 Mar 2025 16:06:53 +0100 Subject: [PATCH 2/4] chore: rename to `has_unscoped_global` --- .../src/compiler/phases/2-analyze/css/css-analyze.js | 10 +++++----- packages/svelte/src/compiler/phases/2-analyze/index.js | 2 +- .../svelte/src/compiler/phases/3-transform/index.js | 4 ++-- packages/svelte/src/compiler/phases/types.d.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 0b83053cef5f..87bc2cc35ae4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -13,7 +13,7 @@ import { is_global, is_unscoped_pseudo_class } from './utils.js'; * { * keyframes: string[]; * rule: AST.CSS.Rule | null; - * has_global_unscoped: { value: boolean }; + * has_unscoped_global: { value: boolean }; * } * >} CssVisitors */ @@ -97,7 +97,7 @@ const css_visitors = { context.state.rule && context.state.rule.block.children.length > 0 ) { - context.state.has_global_unscoped.value = true; + context.state.has_unscoped_global.value = true; } } } @@ -223,7 +223,7 @@ const css_visitors = { is_global_block = true; if (i === 0) { if (is_unscoped_global(context.path) && node.block.children.length > 0) { - context.state.has_global_unscoped.value = true; + context.state.has_unscoped_global.value = true; } } for (let i = idx + 1; i < child.selectors.length; i++) { @@ -329,8 +329,8 @@ export function analyze_css(stylesheet, analysis) { keyframes: analysis.css.keyframes, rule: null, // we need to use an object since state is spread - has_global_unscoped: { value: false } + has_unscoped_global: { value: false } }; walk(stylesheet, css_state, css_visitors); - analysis.css.has_global_unscoped = css_state.has_global_unscoped.value; + analysis.css.has_unscoped_global = css_state.has_unscoped_global.value; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 8e6cc8fd134f..20c2e6c94b18 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -456,7 +456,7 @@ export function analyze_component(root, source, options) { }) : '', keyframes: [], - has_global_unscoped: false + has_unscoped_global: false }, source, undefined_exports: new Map(), diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index 9e02675feaed..87aab46c293f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -22,7 +22,7 @@ export function transform_component(analysis, source, options) { warnings: state.warnings, // set afterwards metadata: { runes: analysis.runes, - hasUnscopedGlobalCss: analysis.css.has_global_unscoped + hasUnscopedGlobalCss: analysis.css.has_unscoped_global }, ast: /** @type {any} */ (null) // set afterwards }; @@ -55,7 +55,7 @@ export function transform_component(analysis, source, options) { warnings: state.warnings, // set afterwards. TODO apply preprocessor sourcemap metadata: { runes: analysis.runes, - hasUnscopedGlobalCss: analysis.css.has_global_unscoped + hasUnscopedGlobalCss: analysis.css.has_unscoped_global }, ast: /** @type {any} */ (null) // set afterwards }; diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index f5b480cb317d..341c5ad157cb 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -73,7 +73,7 @@ export interface ComponentAnalysis extends Analysis { ast: AST.CSS.StyleSheet | null; hash: string; keyframes: string[]; - has_global_unscoped: boolean; + has_unscoped_global: boolean; }; source: string; undefined_exports: Map; From 9f29b81314c307549a0bcb46afd46014a4f8b0ad Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 5 Mar 2025 16:25:02 +0100 Subject: [PATCH 3/4] fix: handle `-global` keyframes --- .../svelte/src/compiler/phases/2-analyze/css/css-analyze.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 87bc2cc35ae4..b1b6fa55e240 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -68,6 +68,11 @@ const css_visitors = { if (is_keyframes_node(node)) { if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) { context.state.keyframes.push(node.prelude); + } else if (node.prelude.startsWith('-global-')) { + // we don't check if the block.children.length because the keyframe is still added even if empty + if (is_unscoped_global(context.path)) { + context.state.has_unscoped_global.value = true; + } } } From ee4869f7c58aa1f5d38414b8dbc786fa86d87357 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 5 Mar 2025 16:26:54 +0100 Subject: [PATCH 4/4] chore: guard the check if the value is already true --- .../src/compiler/phases/2-analyze/css/css-analyze.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index b1b6fa55e240..e6a6f19afbf7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -70,7 +70,7 @@ const css_visitors = { context.state.keyframes.push(node.prelude); } else if (node.prelude.startsWith('-global-')) { // we don't check if the block.children.length because the keyframe is still added even if empty - if (is_unscoped_global(context.path)) { + if (!context.state.has_unscoped_global.value && is_unscoped_global(context.path)) { context.state.has_unscoped_global.value = true; } } @@ -98,6 +98,7 @@ const css_visitors = { if (idx === 0) { if ( + !context.state.has_unscoped_global.value && is_unscoped_global(context.path, context.state.rule) && context.state.rule && context.state.rule.block.children.length > 0 @@ -227,7 +228,11 @@ const css_visitors = { if (idx !== -1) { is_global_block = true; if (i === 0) { - if (is_unscoped_global(context.path) && node.block.children.length > 0) { + if ( + !context.state.has_unscoped_global.value && + is_unscoped_global(context.path) && + node.block.children.length > 0 + ) { context.state.has_unscoped_global.value = true; } }