Repository: sveltejs/svelte (JavaScript)
Date: 2021-07-25 12:39:25+00:00
Task type: Bug Fix

Issue:
Regression introduced in PR #5870
### Describe the bug

Initializing a component into an unmounted div throws
```
Uncaught TypeError: _a.getElementById is not a function
```

### Reproduction

https://svelte.dev/repl/2106c33509344d6784617400c312893d?version=3.40.2
Click on "Hello World" h1.

works in v3.39

The offending lines are
```
 const append_styles_to = get_root_for_styles(target);
    if (!((_a = append_styles_to) === null || _a === void 0 ? void 0 : _a.getElementById(style_sheet_id))) {
```
in svelte/internal/index.mjs (introduced in #5870)
get_root_for_styles does not return document for a detached node

### Logs

```shell
Uncaught TypeError: _a.getElementById is not a function
    at append_styles (eval at handle_message (VM1892 about:srcdoc:13), <anonymous>:41:79)
    at add_css (eval at handle_message (VM1892 about:srcdoc:13), <anonymous>:366:6)
    at init (eval at handle_message (VM1892 about:srcdoc:13), <anonymous>:229:26)
    at new Dynamic (eval at handle_message (VM1892 about:srcdoc:13), <anonymous>:438:7)
    at HTMLHeadingElement.append$1 (eval at handle_message (VM1892 about:srcdoc:13), <anonymous>:467:6)
append_styles @ VM1909:41
add_css @ VM1909:366
init @ VM1909:229
Dynamic @ VM1909:438
append$1 @ VM1909:467
```


### System Info

```shell
System:
    OS: Linux 5.8 Ubuntu 20.04.2 LTS (Focal Fossa)
    CPU: (8) x64 Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz
    Memory: 8.34 GB / 30.96 GB
    Container: Yes
    Shell: 5.8 - /usr/bin/zsh
  Binaries:
    Node: 14.17.0 - /usr/bin/node
    Yarn: 1.22.10 - /usr/bin/yarn
    npm: 6.14.13 - /usr/bin/npm
  Browsers:
    Chrome: 90.0.4430.212
    Firefox: 90.0
```


### Severity

blocking an upgrade


Issue discussion:
I think this might be a component resolution issue when a component is loaded dynamically.

I'm getting this error in dev (but not build) after upgrading to Svelte 3.40.2 in the svelte/kit project

```
Error: ENOENT: no such file or directory, open '/src/components/NavBar/src/components/NavBar/NavBar.svelte'
```

In my project NavBar component is the first component loaded by __layout.svelte. If I remove the NavBar import, I get the error on `src/routes/index.svelte`, which is the root of my project, but now first component loaded after __layout.svelte

Downgrade to 3.39 and everything works.

Someone else has logged a [similar issue](https://github.com/vitejs/vite/issues/4383) to mine with in the Vite project, which is most likely the same thing.

I think this is only happening when components are loaded dynamically, as the example above, my example and the vite issue example all happen with dynamically loaded components


@antstanley : thats a different issue, see https://github.com/sveltejs/kit/issues/2001 



Solution patch:
diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts
--- a/src/runtime/internal/dom.ts
+++ b/src/runtime/internal/dom.ts
@@ -134,9 +134,9 @@ export function append_styles(
 	style_sheet_id: string,
 	styles: string
 ) {
-	const append_styles_to = get_root_for_styles(target);
+	const append_styles_to = get_root_for_style(target);
 
-	if (!append_styles_to?.getElementById(style_sheet_id)) {
+	if (!append_styles_to.getElementById(style_sheet_id)) {
 		const style = element('style');
 		style.id = style_sheet_id;
 		style.textContent = styles;
@@ -144,20 +144,19 @@ export function append_styles(
 	}
 }
 
-export function get_root_for_node(node: Node) {
+export function get_root_for_style(node: Node): ShadowRoot | Document {
 	if (!node) return document;
 
-	return (node.getRootNode ? node.getRootNode() : node.ownerDocument); // check for getRootNode because IE is still supported
-}
-
-function get_root_for_styles(node: Node) {
-	const root = get_root_for_node(node);
-	return (root as ShadowRoot).host ? root as ShadowRoot : root as Document;
+	const root = node.getRootNode ? node.getRootNode() : node.ownerDocument;
+	if ((root as ShadowRoot).host) {
+		return root as ShadowRoot;
+	}
+	return document;
 }
 
 export function append_empty_stylesheet(node: Node) {
 	const style_element = element('style') as HTMLStyleElement;
-	append_stylesheet(get_root_for_styles(node), style_element);
+	append_stylesheet(get_root_for_style(node), style_element);
 	return style_element;
 }
 
diff --git a/src/runtime/internal/style_manager.ts b/src/runtime/internal/style_manager.ts
--- a/src/runtime/internal/style_manager.ts
+++ b/src/runtime/internal/style_manager.ts
@@ -1,4 +1,4 @@
-import { append_empty_stylesheet, get_root_for_node } from './dom';
+import { append_empty_stylesheet, get_root_for_style } from './dom';
 import { raf } from './environment';
 
 interface ExtendedDoc extends Document {
@@ -29,7 +29,7 @@ export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b:
 
 	const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`;
 	const name = `__svelte_${hash(rule)}_${uid}`;
-	const doc = get_root_for_node(node) as unknown as ExtendedDoc;
+	const doc = get_root_for_style(node) as ExtendedDoc;
 	active_docs.add(doc);
 	const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = append_empty_stylesheet(node).sheet as CSSStyleSheet);
 	const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {});


Test patch:
diff --git a/test/runtime/samples/target-dom-detached/App.svelte b/test/runtime/samples/target-dom-detached/App.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/target-dom-detached/App.svelte
@@ -0,0 +1,11 @@
+<script>
+	let name = 'World';
+</script>
+
+<div>Hello {name}</div>
+
+<style>
+	div {
+		color: red;
+	}
+</style>
\ No newline at end of file
diff --git a/test/runtime/samples/target-dom-detached/_config.js b/test/runtime/samples/target-dom-detached/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/target-dom-detached/_config.js
@@ -0,0 +1,16 @@
+export default {
+	skip_if_ssr: true,
+	compileOptions: {
+		cssHash: () => 'svelte-xyz'
+	},
+	async test({ assert, component, target, window }) {
+		assert.htmlEqual(
+			window.document.head.innerHTML,
+			'<style id="svelte-xyz">div.svelte-xyz{color:red}</style>'
+		);
+		assert.htmlEqual(
+			component.div.innerHTML,
+			'<div class="svelte-xyz">Hello World</div>'
+		);
+	}
+};
diff --git a/test/runtime/samples/target-dom-detached/main.svelte b/test/runtime/samples/target-dom-detached/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/target-dom-detached/main.svelte
@@ -0,0 +1,18 @@
+<script>
+	import App from './App.svelte';
+	import { onMount } from 'svelte';
+
+	export let div;
+
+	onMount(() => {
+		div = document.createElement('div');
+
+		const app = new App({
+			target: div
+		});
+
+		return () => {
+			app.$destroy();
+		}
+	});
+</script>
diff --git a/test/runtime/samples/target-dom/App.svelte b/test/runtime/samples/target-dom/App.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/target-dom/App.svelte
@@ -0,0 +1,11 @@
+<script>
+	let name = 'World';
+</script>
+
+<div>Hello {name}</div>
+
+<style>
+	div {
+		color: red;
+	}
+</style>
\ No newline at end of file
diff --git a/test/runtime/samples/target-dom/_config.js b/test/runtime/samples/target-dom/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/target-dom/_config.js
@@ -0,0 +1,16 @@
+export default {
+	skip_if_ssr: true,
+	compileOptions: {
+		cssHash: () => 'svelte-xyz'
+	},
+	async test({ assert, component, target, window }) {
+		assert.htmlEqual(
+			window.document.head.innerHTML,
+			'<style id="svelte-xyz">div.svelte-xyz{color:red}</style>'
+		);
+		assert.htmlEqual(
+			component.div.innerHTML,
+			'<div class="svelte-xyz">Hello World</div>'
+		);
+	}
+};
diff --git a/test/runtime/samples/target-dom/main.svelte b/test/runtime/samples/target-dom/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/target-dom/main.svelte
@@ -0,0 +1,18 @@
+<script>
+	import App from './App.svelte';
+	import { onMount } from 'svelte';
+
+	export let div;
+
+	onMount(() => {
+		const app = new App({
+			target: div
+		});
+
+		return () => {
+			app.$destroy();
+		}
+	});
+</script>
+
+<div bind:this={div} />
\ No newline at end of file
diff --git a/test/runtime/samples/target-shadow-dom/App.svelte b/test/runtime/samples/target-shadow-dom/App.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/target-shadow-dom/App.svelte
@@ -0,0 +1,11 @@
+<script>
+	let name = 'World';
+</script>
+
+<div>Hello {name}</div>
+
+<style>
+	div {
+		color: red;
+	}
+</style>
\ No newline at end of file
diff --git a/test/runtime/samples/target-shadow-dom/_config.js b/test/runtime/samples/target-shadow-dom/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/target-shadow-dom/_config.js
@@ -0,0 +1,13 @@
+export default {
+	skip_if_ssr: true,
+	compileOptions: {
+		cssHash: () => 'svelte-xyz'
+	},
+	async test({ assert, component, target, window }) {
+		assert.htmlEqual(window.document.head.innerHTML, '');
+		assert.htmlEqual(component.div.shadowRoot.innerHTML, `
+			<style id="svelte-xyz">div.svelte-xyz{color:red}</style>
+			<div class="svelte-xyz">Hello World</div>
+		`);
+	}
+};
diff --git a/test/runtime/samples/target-shadow-dom/main.svelte b/test/runtime/samples/target-shadow-dom/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/target-shadow-dom/main.svelte
@@ -0,0 +1,19 @@
+<script>
+	import App from './App.svelte';
+	import { onMount } from 'svelte';
+
+	export let div;
+	onMount(() => {
+		const root = div.attachShadow({ mode: 'open' });
+
+		const app = new App({
+			target: root
+		});
+
+		return () => {
+			app.$destroy();
+		}
+	});
+</script>
+
+<div bind:this={div} />
\ No newline at end of file