Repository: sveltejs/svelte (JavaScript) — Feature

Issue:
Detect when dynamic components are in `components` and not `data`
[This](https://svelte.technology/repl?version=1.49.1&gist=d5a190af6c510194a969ebe458ac2b1f) is incorrect, but it's not immediately obvious why (it took me a while to figure out).

The component constructors need to be in `data`, *not* `components` — [like this](https://svelte.technology/repl?version=1.49.1&gist=b23156a9a1b969a057b2f479ac53c935). Is there a way to detect that mistake and warn on it?


Discussion:
Do they have to be in `data`?
I mean, can we not find a way around of leaving them in `components`.

I suppose this might mean components keeping track of all their children, but that might be better in the long run when HMR is introduced (maybe)

> can we not find a way around of leaving them in components

The short answer is 'no', it's not possible. The longer answer: if you had something like this...

```html
<VirtualList component={{Item}} height=500/>
```

...then inside the `<VirtualList>` component you'd have something like this:

```html
{{#each visibleItems as item}}
  <:Component {component} :item/>
{{/each}}
```

The parent component would have no knowledge that `component` was a special property and that `Item` should therefore come from `components` and not `data`, and the child component wouldn't have access to `Item` *other* than as data.

This is one of those things where something that *initially* seems like it would reduce confusion would end up creating a lot more. It's better if we just have one concept for this, and try and come up with ways to guide people towards the correct understanding — such as a dev-time warning that lets you know that `Item` was on Svelte's internal `expectedProperties` list and is present on `components` (where it is unused) but *not* on the initial `state` object created from `data`.

Resolving patch:
diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts
--- a/src/generators/dom/index.ts
+++ b/src/generators/dom/index.ts
@@ -212,10 +212,12 @@ export default function dom(
 		${generator.metaBindings}
 		${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
 		${options.dev &&
-			Array.from(generator.expectedProperties).map(
-				prop =>
-					`if (!('${prop}' in this._state)) console.warn("${debugName} was created without expected data property '${prop}'");`
-			)}
+			Array.from(generator.expectedProperties).map(prop => {
+				const message = generator.components.has(prop) ?
+					`${debugName} expected to find '${prop}' in \`data\`, but found it in \`components\` instead` :
+					`${debugName} was created without expected data property '${prop}'`;
+				return `if (!('${prop}' in this._state)) console.warn("${message}");`
+			})}
 		${generator.bindingGroups.length &&
 			`this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`}
 
diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts
--- a/src/generators/server-side-rendering/index.ts
+++ b/src/generators/server-side-rendering/index.ts
@@ -232,7 +232,7 @@ export default function ssr(
 		${
 			/__missingComponent/.test(generator.renderCode) && deindent`
 				var __missingComponent = {
-					render: () => ''
+					_render: () => ''
 				};
 			`
 		}
diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts
--- a/src/validate/html/validateElement.ts
+++ b/src/validate/html/validateElement.ts
@@ -16,6 +16,10 @@ export default function validateElement(
 	const isComponent =
 		node.name === ':Self' || node.name === ':Component' || validator.components.has(node.name);
 
+	if (isComponent) {
+		validator.used.components.add(node.name);
+	}
+
 	if (!isComponent && /^[A-Z]/.test(node.name[0])) {
 		// TODO upgrade to validator.error in v2
 		validator.warn(`${node.name} component is not defined`, node.start);
diff --git a/src/validate/index.ts b/src/validate/index.ts
--- a/src/validate/index.ts
+++ b/src/validate/index.ts
@@ -35,6 +35,10 @@ export class Validator {
 	transitions: Map<string, Node>;
 	slots: Set<string>;
 
+	used: {
+		components: Set<string>
+	};
+
 	constructor(parsed: Parsed, source: string, options: CompileOptions) {
 		this.source = source;
 		this.filename = options.filename;
@@ -50,6 +54,10 @@ export class Validator {
 		this.helpers = new Map();
 		this.transitions = new Map();
 		this.slots = new Set();
+
+		this.used = {
+			components: new Set()
+		};
 	}
 
 	error(message: string, pos: number) {
@@ -114,6 +122,22 @@ export default function validate(
 		if (parsed.html) {
 			validateHtml(validator, parsed.html);
 		}
+
+		// need to do a second pass of the JS, now that we've analysed the markup
+		if (parsed.js && validator.defaultExport) {
+			const components = validator.defaultExport.declaration.properties.find(prop => prop.key.name === 'components');
+			if (components) {
+				components.value.properties.forEach(prop => {
+					const { name } = prop.key;
+					if (!validator.used.components.has(name)) {
+						validator.warn(
+							`The ${name} component is unused`,
+							prop.start
+						);
+					}
+				});
+			}
+		}
 	} catch (err) {
 		if (onerror) {
 			onerror(err);


Test patch:
diff --git a/test/runtime/samples/dev-warning-dynamic-components-misplaced/Bar.html b/test/runtime/samples/dev-warning-dynamic-components-misplaced/Bar.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/dev-warning-dynamic-components-misplaced/Bar.html
@@ -0,0 +1 @@
+Bar
\ No newline at end of file
diff --git a/test/runtime/samples/dev-warning-dynamic-components-misplaced/Foo.html b/test/runtime/samples/dev-warning-dynamic-components-misplaced/Foo.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/dev-warning-dynamic-components-misplaced/Foo.html
@@ -0,0 +1 @@
+Foo
\ No newline at end of file
diff --git a/test/runtime/samples/dev-warning-dynamic-components-misplaced/_config.js b/test/runtime/samples/dev-warning-dynamic-components-misplaced/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/dev-warning-dynamic-components-misplaced/_config.js
@@ -0,0 +1,14 @@
+export default {
+	dev: true,
+
+	data: {
+		x: true
+	},
+
+	html: '',
+
+	warnings: [
+		`<Main$> expected to find 'Foo' in \`data\`, but found it in \`components\` instead`,
+		`<Main$> expected to find 'Bar' in \`data\`, but found it in \`components\` instead`
+	]
+};
\ No newline at end of file
diff --git a/test/runtime/samples/dev-warning-dynamic-components-misplaced/main.html b/test/runtime/samples/dev-warning-dynamic-components-misplaced/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/dev-warning-dynamic-components-misplaced/main.html
@@ -0,0 +1,13 @@
+<:Component {x ? Foo : Bar}/>
+
+<script>
+	import Foo from './Foo.html';
+	import Bar from './Bar.html';
+
+	export default {
+		components: {
+			Foo,
+			Bar
+		}
+	};
+</script>
\ No newline at end of file
diff --git a/test/validator/samples/component-unused/input.html b/test/validator/samples/component-unused/input.html
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/component-unused/input.html
@@ -0,0 +1,11 @@
+<script>
+	import Foo from './Foo.html';
+	import Bar from './Bar.html';
+
+	export default {
+		components: {
+			Foo,
+			Bar
+		}
+	};
+</script>
\ No newline at end of file
diff --git a/test/validator/samples/component-unused/warnings.json b/test/validator/samples/component-unused/warnings.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/component-unused/warnings.json
@@ -0,0 +1,18 @@
+[
+	{
+		"message": "The Foo component is unused",
+		"loc": {
+			"line": 7,
+			"column": 3
+		},
+		"pos": 109
+	},
+	{
+		"message": "The Bar component is unused",
+		"loc": {
+			"line": 8,
+			"column": 3
+		},
+		"pos": 117
+	}
+]
diff --git a/test/validator/samples/properties-components-should-be-capitalised/input.html b/test/validator/samples/properties-components-should-be-capitalised/input.html
--- a/test/validator/samples/properties-components-should-be-capitalised/input.html
+++ b/test/validator/samples/properties-components-should-be-capitalised/input.html
@@ -1,4 +1,4 @@
-<div></div>
+<foo></foo>
 
 <script>
 	export default {