Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plenty-hotels-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: add `hasUnscopedGlobalCss` to `compile` metadata
54 changes: 52 additions & 2 deletions packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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<AST.CSS.Node>} path
Expand All @@ -42,6 +68,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.has_unscoped_global.value ||= is_unscoped_global(context.path);
}
}

Expand All @@ -64,6 +93,13 @@ const css_visitors = {
}
}
}

if (idx === 0) {
context.state.has_unscoped_global.value ||=
!!context.state.rule &&
context.state.rule.block.children.length > 0 &&
is_unscoped_global(context.path, context.state.rule);
}
}
}

Expand Down Expand Up @@ -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) {
Expand All @@ -184,6 +221,12 @@ const css_visitors = {

if (idx !== -1) {
is_global_block = true;

if (i === 0) {
context.state.has_unscoped_global.value ||=
node.block.children.length > 0 && is_unscoped_global(context.path);
}

for (let i = idx + 1; i < child.selectors.length; i++) {
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
Expand Down Expand Up @@ -283,5 +326,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;
}
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,8 @@ export function analyze_component(root, source, options) {
hash
})
: '',
keyframes: []
keyframes: [],
has_unscoped_global: false
},
source,
undefined_exports: new Map(),
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/phases/3-transform/css/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_unscoped_global
};

merge_with_preprocessor_map(css, options, css.map.sources[0]);
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/phases/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Node>;
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions packages/svelte/tests/css/samples/global-keyframes/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { test } from '../../test';

export default test({
hasGlobal: true
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { test } from '../../test';

export default test({
warnings: []
warnings: [],

hasGlobal: false
});
5 changes: 5 additions & 0 deletions packages/svelte/tests/css/samples/global/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { test } from '../../test';

export default test({
hasGlobal: true
});
9 changes: 9 additions & 0 deletions packages/svelte/tests/css/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface CssTest extends BaseTest {
compileOptions?: Partial<CompileOptions>;
warnings?: Warning[];
props?: Record<string, any>;
hasGlobal?: boolean;
}

/**
Expand Down Expand Up @@ -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();

Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,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:
Expand Down
Loading