Repository: sveltejs/svelte (JavaScript)
Date: 2021-02-03 15:15:26+00:00
Task type: Bug Fix

Issue:
:host <selector> query incorrectly removed as unused when with customElement / web components
## Is this about svelte@next? This project is currently in a pre-release stage and breaking changes may occur at any time. Please do not post any kind of bug reports or questions on GitHub about it.

No

**Describe the bug**
A clear and concise description of what the bug is.

:host > div

styles are incorrectly flagged by Svelte as being unused, and removed.

**Logs**
Please include browser console and server logs around the time this bug occurred.

**To Reproduce**
To help us help you, if you've found a bug please consider the following:

REPL: https://svelte.dev/repl/a3e9faaa04fb41b9a8eda898c92d088d?version=3.32.1

You may need to manually toggle the `customElement` option - it doesn't seem to stick for me - not sure what I'm doing wrong. 

Occasionally, this won't be possible, and that's fine – we still appreciate you raising the issue. But please understand that Svelte is run by unpaid volunteers in their free time, and issues that follow these instructions will get fixed faster.

**Expected behavior**
A clear and concise description of what you expected to happen.

Style rules should not be removed.

**Stacktraces**
If you have a stack trace to include, we recommend putting inside a `<details>` block for the sake of the thread's readability:

<details>
  <summary>Stack trace</summary>

  Stack trace goes here...
</details>

**Information about your Svelte project:**
To make your life easier, just run `npx envinfo --system --npmPackages svelte,rollup,webpack --binaries --browsers` and paste the output here.

- Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10)

Chrome

- Your operating system: (e.x. OS X 10, Ubuntu Linux 19.10, Windows XP, etc)

Mac

- Svelte version (Please check you can reproduce the issue with the latest release!)

Most recent

- Whether your project uses Webpack or Rollup

webpack - see below

**Severity**
How severe an issue is this bug to you? Is this annoying, blocking some users, blocking an upgrade or blocking your usage of Svelte entirely?

Severe 

Note: the more honest and specific you are here the more we will take you seriously.

**Additional context**

What's weird is, with my own setup, with webpack, `:host > *` is NOT removed, but in the repl, even that is removed. To be clear though, none of these rules should be removed. `:host > (selector that matches top-level element in the component)` is perfectly valid.

The best workaround would be any way to just shut off all of Svelte's removal of what it perceives as unused selectors. This is crucial for custom element work since it seems the :global helper is completely ignored. 

I should add that, with this repl, `:host > h1` could easily be replaced with just `h1 { }`, or with an added css class to disambiguate from other h1's in the component. But this workaround becomes impossible when you need to add selectors to the `:host` query, ie

`:host([some-attr]) > div { }`



Issue discussion:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Svelte changelog
 
+## Unreleased
+
+* Fix scoping of selectors with `:global()` and `~` sibling combinators ([#5499](https://github.com/sveltejs/svelte/issues/5499))
+* Fix removal of `:host` selectors as unused when compiling to a custom element ([#5946](https://github.com/sveltejs/svelte/issues/5946))
+
 ## 3.32.1
 
 * Warn when using `module` variables reactively, and close weird reactivity loophole ([#5847](https://github.com/sveltejs/svelte/pull/5847))
diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts
--- a/src/compiler/compile/css/Selector.ts
+++ b/src/compiler/compile/css/Selector.ts
@@ -84,7 +84,7 @@ export default class Selector {
 			while (i--) {
 				const selector = block.selectors[i];
 				if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') {
-					if (selector.name !== 'root') {
+					if (selector.name !== 'root' && selector.name !== 'host') {
 						if (i === 0) code.prependRight(selector.start, attr);
 					}
 					continue;
@@ -162,7 +162,10 @@ function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]):
 	if (!block) return false;
 
 	if (!node) {
-		return block.global && blocks.every(block => block.global);
+		return (
+      (block.global && blocks.every(block => block.global)) ||
+      (block.host && blocks.length === 0)
+    );
 	}
 
 	switch (block_might_apply_to_node(block, node)) {
@@ -182,6 +185,11 @@ function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]):
 					continue;
 				}
 
+				if (ancestor_block.host) {
+					to_encapsulate.push({ node, block });
+					return true;
+				}
+
 				let parent = node;
 				while (parent = get_element_parent(parent)) {
 					if (block_might_apply_to_node(ancestor_block, parent) !== BlockAppliesToNode.NotPossible) {
@@ -211,6 +219,19 @@ function apply_selector(blocks: Block[], node: Element, to_encapsulate: any[]):
 		} else if (block.combinator.name === '+' || block.combinator.name === '~') {
 			const siblings = get_possible_element_siblings(node, block.combinator.name === '+');
 			let has_match = false;
+
+			// NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the 
+			// css-tree limitation that does not parse the inner selector of :global
+			// so unless we are sure there will be no sibling to match, we will consider it as matched
+			const has_global = blocks.some(block => block.global);
+			if (has_global) {
+				if (siblings.size === 0 && get_element_parent(node) !== null) {
+					return false;
+				}
+				to_encapsulate.push({ node, block });
+				return true;
+			}
+
 			for (const possible_sibling of siblings.keys()) {
 				if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) {
 					to_encapsulate.push({ node, block });
@@ -236,6 +257,10 @@ function block_might_apply_to_node(block: Block, node: Element): BlockAppliesToN
 		const selector = block.selectors[i];
 		const name = typeof selector.name === 'string' && selector.name.replace(/\\(.)/g, '$1');
 
+		if (selector.type === 'PseudoClassSelector' && name === 'host') {
+			return BlockAppliesToNode.NotPossible;
+		}
+
 		if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') {
 			continue;
 		}
@@ -541,6 +566,7 @@ function loop_child(children: INode[], adjacent_only: boolean) {
 
 class Block {
 	global: boolean;
+	host: boolean;
 	combinator: CssNode;
 	selectors: CssNode[]
 	start: number;
@@ -550,6 +576,7 @@ class Block {
 	constructor(combinator: CssNode) {
 		this.combinator = combinator;
 		this.global = false;
+		this.host = false;
 		this.selectors = [];
 
 		this.start = null;
@@ -562,6 +589,7 @@ class Block {
 		if (this.selectors.length === 0) {
 			this.start = selector.start;
 			this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global';
+			this.host = selector.type === 'PseudoClassSelector' && selector.name === 'host';
 		}
 
 		this.selectors.push(selector);


Test patch:
diff --git a/test/css/samples/host/_config.js b/test/css/samples/host/_config.js
new file mode 100644
--- /dev/null
+++ b/test/css/samples/host/_config.js
@@ -0,0 +1,27 @@
+export default {
+	warnings: [
+		{
+			code: 'css-unused-selector',
+			message: 'Unused CSS selector ":host > span"',
+			pos: 147,
+			start: {
+				character: 147,
+				column: 1,
+				line: 18
+			},
+			end: {
+				character: 159,
+				column: 13,
+				line: 18
+			},
+			frame: `
+			16:   }
+			17:
+			18:   :host > span {
+			      ^
+			19:     color: red;
+			20:   }
+      `
+		}
+	]
+};
diff --git a/test/css/samples/host/expected.css b/test/css/samples/host/expected.css
new file mode 100644
--- /dev/null
+++ b/test/css/samples/host/expected.css
@@ -0,0 +1 @@
+:host h1.svelte-xyz{color:red}:host>h1.svelte-xyz{color:red}:host>.svelte-xyz{color:red}:host span.svelte-xyz{color:red}
\ No newline at end of file
diff --git a/test/css/samples/host/input.svelte b/test/css/samples/host/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/css/samples/host/input.svelte
@@ -0,0 +1,27 @@
+<style>
+  :host h1 {
+    color: red;
+  }
+
+  :host > h1 {
+    color: red;
+  }
+
+  :host > * {
+    color: red;
+	}
+
+	:host span {
+		color: red;
+	}
+		
+	:host > span {
+		color: red;
+	}
+</style>
+
+<h1>Hello!</h1>
+
+<div>
+  <span>World!</span>
+</div>
diff --git a/test/css/samples/siblings-combinator-global/_config.js b/test/css/samples/siblings-combinator-global/_config.js
new file mode 100644
--- /dev/null
+++ b/test/css/samples/siblings-combinator-global/_config.js
@@ -0,0 +1,50 @@
+export default {
+	warnings: [
+		{
+			code: 'css-unused-selector',
+			message: 'Unused CSS selector ":global(input) + span"',
+			pos: 239,
+			start: {
+				character: 239,
+				column: 2,
+				line: 9
+			},
+			end: {
+				character: 260,
+				column: 23,
+				line: 9
+			},
+			frame: `
+				 7:   :global(input) ~ p { color: red; }
+				 8:
+				 9:   :global(input) + span { color: red; }
+				      ^
+				10:   :global(input) ~ span { color: red; }
+				11: </style>
+			`
+		},
+		{
+			code: 'css-unused-selector',
+			message: 'Unused CSS selector ":global(input) ~ span"',
+			pos: 279,
+			start: {
+				character: 279,
+				column: 2,
+				line: 10
+			},
+			end: {
+				character: 300,
+				column: 23,
+				line: 10
+			},
+			frame: `
+				 8:
+				 9:   :global(input) + span { color: red; }
+				10:   :global(input) ~ span { color: red; }
+				      ^
+				11: </style>
+				12:
+			`
+		}
+	]
+};
diff --git a/test/css/samples/siblings-combinator-global/expected.css b/test/css/samples/siblings-combinator-global/expected.css
new file mode 100644
--- /dev/null
+++ b/test/css/samples/siblings-combinator-global/expected.css
@@ -0,0 +1 @@
+input+div.svelte-xyz{color:red}input~div.svelte-xyz{color:red}input+h1.svelte-xyz{color:red}input~h1.svelte-xyz{color:red}input+p.svelte-xyz{color:red}input~p.svelte-xyz{color:red}
\ No newline at end of file
diff --git a/test/css/samples/siblings-combinator-global/input.svelte b/test/css/samples/siblings-combinator-global/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/css/samples/siblings-combinator-global/input.svelte
@@ -0,0 +1,21 @@
+<style>
+  :global(input) + div { color: red; }
+  :global(input) ~ div { color: red; }
+  :global(input) + h1 { color: red; }
+  :global(input) ~ h1 { color: red; }
+  :global(input) + p { color: red; }
+  :global(input) ~ p { color: red; }
+
+  :global(input) + span { color: red; }
+  :global(input) ~ span { color: red; }
+</style>
+
+<h1>Hello!</h1>
+
+<div>
+  <span>World!</span>
+</div>
+
+{#each [] as _}
+  <p />
+{/each}
\ No newline at end of file