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 <rich.harris@vercel.com>
pull/15731/head
Paolo Ricciuti 5 months ago committed by GitHub
parent ec1d85c89e
commit 01171096ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: add `css.hasGlobal` to `compile` output

@ -118,6 +118,7 @@ function read_rule(parser) {
metadata: {
parent_rule: null,
has_local_selectors: false,
has_global_selectors: false,
is_global_block: false
}
};
@ -342,6 +343,7 @@ function read_selector(parser, inside_pseudo_class = false) {
children,
metadata: {
rule: null,
is_global: false,
used: false
}
};

@ -7,13 +7,15 @@ import { is_keyframes_node } from '../../css.js';
import { is_global, is_unscoped_pseudo_class } from './utils.js';
/**
* @typedef {Visitors<
* AST.CSS.Node,
* {
* keyframes: string[];
* rule: AST.CSS.Rule | null;
* }
* >} CssVisitors
* @typedef {{
* keyframes: string[];
* rule: AST.CSS.Rule | null;
* analysis: ComponentAnalysis;
* }} CssState
*/
/**
* @typedef {Visitors<AST.CSS.Node, CssState>} CssVisitors
*/
/**
@ -28,6 +30,15 @@ function is_global_block_selector(simple_selector) {
);
}
/**
* @param {AST.SvelteNode[]} path
*/
function is_unscoped(path) {
return path
.filter((node) => node.type === 'Rule')
.every((node) => node.metadata.has_global_selectors);
}
/**
*
* @param {Array<AST.CSS.Node>} path
@ -42,6 +53,9 @@ 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
context.state.analysis.css.has_global ||= is_unscoped(context.path);
}
}
@ -99,10 +113,12 @@ const css_visitors = {
node.metadata.rule = context.state.rule;
node.metadata.used ||= node.children.every(
node.metadata.is_global = node.children.every(
({ metadata }) => metadata.is_global || metadata.is_global_like
);
node.metadata.used ||= node.metadata.is_global;
if (
node.metadata.rule?.metadata.parent_rule &&
node.children[0]?.selectors[0]?.type === 'NestingSelector'
@ -190,6 +206,7 @@ const css_visitors = {
if (idx !== -1) {
is_global_block = true;
for (let i = idx + 1; i < child.selectors.length; i++) {
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
@ -242,16 +259,26 @@ const css_visitors = {
}
}
context.next({
...context.state,
rule: node
});
const state = { ...context.state, rule: node };
node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
return selector.children.some(
({ metadata }) => !metadata.is_global && !metadata.is_global_like
);
});
// visit selector list first, to populate child selector metadata
context.visit(node.prelude, state);
for (const selector of node.prelude.children) {
node.metadata.has_global_selectors ||= selector.metadata.is_global;
node.metadata.has_local_selectors ||= !selector.metadata.is_global;
}
// if this rule has a ComplexSelector whose RelativeSelector children are all
// `:global(...)`, and the rule contains declarations (rather than just
// nested rules) then the component as a whole includes global CSS
context.state.analysis.css.has_global ||=
node.metadata.has_global_selectors &&
node.block.children.filter((child) => child.type === 'Declaration').length > 0 &&
is_unscoped(context.path);
// visit block list, so parent rule metadata is populated
context.visit(node.block, state);
},
NestingSelector(node, context) {
const rule = /** @type {AST.CSS.Rule} */ (context.state.rule);
@ -289,5 +316,12 @@ const css_visitors = {
* @param {ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {
walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
/** @type {CssState} */
const css_state = {
keyframes: analysis.css.keyframes,
rule: null,
analysis
};
walk(stylesheet, css_state, css_visitors);
}

@ -456,7 +456,8 @@ export function analyze_component(root, source, options) {
hash
})
: '',
keyframes: []
keyframes: [],
has_global: false
},
source,
undefined_exports: new Map(),

@ -59,7 +59,8 @@ export function render_stylesheet(source, analysis, options) {
// generateMap takes care of calculating source relative to file
source: options.filename,
file: options.cssOutputFilename || options.filename
})
}),
hasGlobal: analysis.css.has_global
};
merge_with_preprocessor_map(css, options, css.map.sources[0]);

@ -74,6 +74,7 @@ export interface ComponentAnalysis extends Analysis {
ast: AST.CSS.StyleSheet | null;
hash: string;
keyframes: string[];
has_global: boolean;
};
source: string;
undefined_exports: Map<string, Node>;

@ -34,6 +34,10 @@ export namespace _CSS {
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
/**
* `true` if the rule contains a ComplexSelector whose RelativeSelectors are all global or global-like
*/
has_global_selectors: boolean;
/**
* `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped
*/
@ -64,6 +68,7 @@ export namespace _CSS {
/** @internal */
metadata: {
rule: null | Rule;
is_global: boolean;
/** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */
used: boolean;
};

@ -18,6 +18,8 @@ export interface CompileResult {
code: string;
/** A source map */
map: SourceMap;
/** Whether or not the CSS includes global rules */
hasGlobal: boolean;
};
/**
* An array of warning objects that were generated during compilation. Each warning has several properties:

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
hasGlobal: true
});

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
hasGlobal: false
});

@ -0,0 +1,12 @@
div.svelte-xyz {
.whatever {
color: green;
}
}
.whatever {
div.svelte-xyz {
color: green;
}
}

@ -0,0 +1,15 @@
<div>{@html whatever}</div>
<style>
div {
:global(.whatever) {
color: green;
}
}
:global(.whatever) {
div {
color: green;
}
}
</style>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
hasGlobal: false
});

@ -0,0 +1,8 @@
div.svelte-xyz .whatever {
color: green;
}
.whatever div.svelte-xyz {
color: green;
}

@ -0,0 +1,11 @@
<div>{@html whatever}</div>
<style>
div :global(.whatever) {
color: green;
}
:global(.whatever) div {
color: green;
}
</style>

@ -1,5 +1,7 @@
import { test } from '../../test';
export default test({
warnings: []
warnings: [],
hasGlobal: false
});

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
hasGlobal: true
});

@ -34,6 +34,7 @@ interface CssTest extends BaseTest {
compileOptions?: Partial<CompileOptions>;
warnings?: Warning[];
props?: Record<string, any>;
hasGlobal?: boolean;
}
/**
@ -78,6 +79,14 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
// assert_html_equal(actual_ssr, expected.html);
}
if (config.hasGlobal !== undefined) {
const metadata = JSON.parse(
fs.readFileSync(`${cwd}/_output/client/input.svelte.css.json`, 'utf-8')
);
assert.equal(metadata.hasGlobal, config.hasGlobal);
}
const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim();
const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim();

@ -146,6 +146,10 @@ export async function compile_directory(
if (compiled.css) {
write(`${output_dir}/${file}.css`, compiled.css.code);
write(
`${output_dir}/${file}.css.json`,
JSON.stringify({ hasGlobal: compiled.css.hasGlobal })
);
if (output_map) {
write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t'));
}

@ -753,6 +753,8 @@ declare module 'svelte/compiler' {
code: string;
/** A source map */
map: SourceMap;
/** Whether or not the CSS includes global rules */
hasGlobal: boolean;
};
/**
* An array of warning objects that were generated during compilation. Each warning has several properties:

Loading…
Cancel
Save