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..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 @@ -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_unscoped_global: { 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 @@ -42,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 (!context.state.has_unscoped_global.value && is_unscoped_global(context.path)) { + context.state.has_unscoped_global.value = true; + } } } @@ -64,6 +95,17 @@ 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 + ) { + context.state.has_unscoped_global.value = true; + } + } } } @@ -174,7 +216,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 +227,15 @@ const css_visitors = { if (idx !== -1) { is_global_block = true; + if (i === 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; + } + } for (let i = idx + 1; i < child.selectors.length; i++) { walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { ComplexSelector(node) { @@ -283,5 +335,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_unscoped_global: { value: false } + }; + walk(stylesheet, css_state, css_visitors); + 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 322293bf6b91..20c2e6c94b18 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_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 f96fd64ec7a9..87aab46c293f 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_unscoped_global }, 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_unscoped_global }, 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..341c5ad157cb 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_unscoped_global: 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;