The exception are `:not` selectors with descendant selectors, as that means "look up the tree" and we need to scope all ancestor elements in that case.

pull/14177/head
Simon Holthausen 2 years ago
parent 1089c7b157
commit 32304b2c77

@ -162,9 +162,7 @@ const visitors = {
}; };
/** /**
* Discard trailing `:global(...)` selectors without a `:has/is/where(...)` modifier, these are unused for scoping purposes * Discard trailing `:global(...)` selectors without a `:has/is/where/not(...)` modifier, these are unused for scoping purposes
* `:not(...)` modifiers are not considered, because they should stay unscoped, because scoping them would achieve the
* opposite of what we want, because they are then _more_ likely to bleed out of the component.
* @param {Compiler.Css.ComplexSelector} node * @param {Compiler.Css.ComplexSelector} node
*/ */
function truncate(node) { function truncate(node) {
@ -174,13 +172,16 @@ function truncate(node) {
// not after a :global selector // not after a :global selector
!metadata.is_global_like && !metadata.is_global_like &&
!(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) && !(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) &&
// not a :global(...) without a :has/is/where(...) modifier // not a :global(...) without a :has/is/where/not(...) modifier
(!metadata.is_global || (!metadata.is_global ||
selectors.some( selectors.some(
(selector) => (selector) =>
selector.type === 'PseudoClassSelector' && selector.type === 'PseudoClassSelector' &&
selector.args !== null && selector.args !== null &&
(selector.name === 'has' || selector.name === 'is' || selector.name === 'where') (selector.name === 'has' ||
selector.name === 'is' ||
selector.name === 'where' ||
selector.name === 'not')
)) ))
); );
}); });
@ -511,9 +512,34 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
// We came across a :global, everything beyond it is global and therefore a potential match // We came across a :global, everything beyond it is global and therefore a potential match
if (name === 'global' && selector.args === null) return true; if (name === 'global' && selector.args === null) return true;
// We ignore :not(...) because its contents should stay unscoped. Scoping them would achieve the // :not(...) contents should stay unscoped. Scoping them would achieve the opposite of what we want,
// opposite of what we want, because they are then _more_ likely to bleed out of the component, // because they are then _more_ likely to bleed out of the component. The exception is complex selectors
// because there would be more chances of the inner selector not matching, which means `:not` matches. // with descendants, in which case we scope them all.
if (name === 'not' && selector.args) {
for (const complex_selector of selector.args.children) {
complex_selector.metadata.used = true;
const relative = truncate(complex_selector);
if (complex_selector.children.length > 1) {
// foo:not(bar foo) means that bar is an ancestor of foo (side note: ending with foo is the only way the selector make sense).
// We can't fully check if that actually matches with our current algorithm, so we just assume it does.
// The result may not match a real element, so the only drawback is the missing prune.
for (const selector of relative) {
selector.metadata.scoped = true;
}
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */
let el = element;
while (el) {
el.metadata.scoped = true;
el = get_element_parent(el);
}
}
}
break;
}
if ((name === 'is' || name === 'where') && selector.args) { if ((name === 'is' || name === 'where') && selector.args) {
let matched = false; let matched = false;

@ -355,19 +355,9 @@ const visitors = {
context.state.specificity.bumped = before_bumped; context.state.specificity.bumped = before_bumped;
}, },
PseudoClassSelector(node, context) { PseudoClassSelector(node, context) {
if (node.name === 'is' || node.name === 'where' || node.name === 'has') { if (node.name === 'is' || node.name === 'where' || node.name === 'has' || node.name === 'not') {
context.next(); context.next();
} }
if (node.name === 'not' && node.args) {
for (const complex_selector of node.args.children) {
for (const relative_selector of complex_selector.children) {
if (relative_selector.metadata.is_global) {
const global = /** @type {Css.PseudoClassSelector} */ (relative_selector.selectors[0]);
remove_global_pseudo_class(global, relative_selector.combinator, context.state);
}
}
}
}
} }
}; };

@ -9,11 +9,21 @@
color: green; color: green;
} }
.x .svelte-xyz:not(p) { .x .svelte-xyz:not(p) {
color: red; /* TODO would be nice to prune this one day */ color: green;
} }
.x:not(p) { .x:not(p) {
color: red; /* TODO would be nice to prune this one day */ color: green;
} }
.x .svelte-xyz:not(.unused) { .x .svelte-xyz:not(.unused) {
color: green; color: green;
} }
span:not(p.svelte-xyz span:where(.svelte-xyz)) {
color: green;
}
span.svelte-xyz:not(p span) {
color: green;
}
span:not(p span) {
color: green;
}

@ -1 +1,3 @@
<p class="foo svelte-xyz">foo</p> <p class="bar svelte-xyz">bar</p> <p class="foo svelte-xyz">foo</p>
<p class="bar svelte-xyz">bar <span class="svelte-xyz">baz</span></p>
<span class="svelte-xyz">buzz</span>

@ -1,5 +1,9 @@
<p class="foo">foo</p> <p class="foo">foo</p>
<p class="bar">bar</p> <p class="bar">
bar
<span>baz</span>
</p>
<span>buzz</span>
<style> <style>
:not(:global(.foo)) { :not(:global(.foo)) {
@ -12,12 +16,22 @@
color: green; color: green;
} }
:global(.x) :not(p) { :global(.x) :not(p) {
color: red; /* TODO would be nice to prune this one day */ color: green;
} }
:global(.x):not(p) { :global(.x):not(p) {
color: red; /* TODO would be nice to prune this one day */ color: green;
} }
:global(.x) :not(.unused) { :global(.x) :not(.unused) {
color: green; color: green;
} }
:global(span):not(p span) {
color: green;
}
span:not(:global(p span)) {
color: green;
}
:global(span:not(p span)) {
color: green;
}
</style> </style>

@ -4,30 +4,30 @@ export default test({
warnings: [ warnings: [
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector "p :not(.foo)"', message: 'Unused CSS selector "span :not(.foo)"',
start: { start: {
line: 22, line: 26,
column: 1, column: 1,
character: 291 character: 276
}, },
end: { end: {
line: 22, line: 26,
column: 13, column: 16,
character: 303 character: 291
} }
}, },
{ {
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector "p :not(.unused)"', message: 'Unused CSS selector "span :not(.unused)"',
start: { start: {
line: 25, line: 29,
column: 1, column: 1,
character: 324 character: 312
}, },
end: { end: {
line: 25, line: 29,
column: 16, column: 19,
character: 339 character: 330
} }
} }
] ]

@ -6,7 +6,7 @@
color: green; color: green;
} }
.svelte-xyz:not(p) { .svelte-xyz:not(p) {
color: red; /* TODO would be nice to mark this as unused someday */ color: green;
} }
.svelte-xyz:not(.foo):not(.unused) { .svelte-xyz:not(.foo):not(.unused) {
@ -16,9 +16,13 @@
p.svelte-xyz:not(.foo) { p.svelte-xyz:not(.foo) {
color: green; color: green;
} }
/* (unused) p :not(.foo) { /* (unused) span :not(.foo) {
color: red; color: red;
}*/ }*/
/* (unused) p :not(.unused) { /* (unused) span :not(.unused) {
color: red; color: red;
}*/ }*/
span.svelte-xyz:not(p:where(.svelte-xyz) span:where(.svelte-xyz)) {
color: green;
}

@ -1 +1,3 @@
<p class="foo svelte-xyz">foo</p> <p class="bar svelte-xyz">bar</p> <p class="foo svelte-xyz">foo</p>
<p class="bar svelte-xyz">bar <span class="svelte-xyz">baz</span></p>
<span class="svelte-xyz">buzz</span>

@ -1,5 +1,9 @@
<p class="foo">foo</p> <p class="foo">foo</p>
<p class="bar">bar</p> <p class="bar">
bar
<span>baz</span>
</p>
<span>buzz</span>
<style> <style>
:not(.foo) { :not(.foo) {
@ -9,7 +13,7 @@
color: green; color: green;
} }
:not(p) { :not(p) {
color: red; /* TODO would be nice to mark this as unused someday */ color: green;
} }
:not(.foo):not(.unused) { :not(.foo):not(.unused) {
@ -19,10 +23,14 @@
p:not(.foo) { p:not(.foo) {
color: green; color: green;
} }
p :not(.foo) { span :not(.foo) {
color: red; color: red;
} }
p :not(.unused) { span :not(.unused) {
color: red; color: red;
} }
span:not(p span) {
color: green;
}
</style> </style>

@ -32,30 +32,17 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
await compile_directory(cwd, 'client', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); await compile_directory(cwd, 'client', { cssHash: () => 'svelte-xyz', ...config.compileOptions });
await compile_directory(cwd, 'server', { cssHash: () => 'svelte-xyz', ...config.compileOptions }); await compile_directory(cwd, 'server', { cssHash: () => 'svelte-xyz', ...config.compileOptions });
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();
assert.equal(dom_css, ssr_css);
const dom_warnings = load_warnings(`${cwd}/_output/client/input.svelte.warnings.json`);
const ssr_warnings = load_warnings(`${cwd}/_output/server/input.svelte.warnings.json`);
const expected_warnings = (config.warnings || []).map(normalize_warning);
assert.deepEqual(dom_warnings, ssr_warnings);
assert.deepEqual(dom_warnings.map(normalize_warning), expected_warnings);
const expected = { const expected = {
html: try_read_file(`${cwd}/expected.html`), html: try_read_file(`${cwd}/expected.html`),
css: try_read_file(`${cwd}/expected.css`) css: try_read_file(`${cwd}/expected.css`)
}; };
assert.equal(dom_css.trim().replace(/\r\n/g, '\n'), (expected.css ?? '').trim());
// we do this here, rather than in the expected.html !== null // we do this here, rather than in the expected.html !== null
// block, to verify that valid code was generated // block, to verify that valid code was generated
const ClientComponent = (await import(`${cwd}/_output/client/input.svelte.js`)).default; const ClientComponent = (await import(`${cwd}/_output/client/input.svelte.js`)).default;
const ServerComponent = (await import(`${cwd}/_output/server/input.svelte.js`)).default; const ServerComponent = (await import(`${cwd}/_output/server/input.svelte.js`)).default;
// verify that the right elements have scoping selectors // verify that the right elements have scoping selectors (do this first to ensure all actual files are written to disk)
if (expected.html !== null) { if (expected.html !== null) {
const target = window.document.createElement('main'); const target = window.document.createElement('main');
@ -75,6 +62,19 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
// const actual_ssr = ServerComponent.render(config.props).html; // const actual_ssr = ServerComponent.render(config.props).html;
// assert_html_equal(actual_ssr, expected.html); // assert_html_equal(actual_ssr, expected.html);
} }
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();
assert.equal(dom_css, ssr_css);
const dom_warnings = load_warnings(`${cwd}/_output/client/input.svelte.warnings.json`);
const ssr_warnings = load_warnings(`${cwd}/_output/server/input.svelte.warnings.json`);
const expected_warnings = (config.warnings || []).map(normalize_warning);
assert.deepEqual(dom_warnings, ssr_warnings);
assert.deepEqual(dom_warnings.map(normalize_warning), expected_warnings);
assert.equal(dom_css.trim().replace(/\r\n/g, '\n'), (expected.css ?? '').trim());
}); });
export { test }; export { test };

Loading…
Cancel
Save