From 14ecedf33b04d74396372e93ffb0458b36b57761 Mon Sep 17 00:00:00 2001
From: Paolo Ricciuti <ricciutipaolo@gmail.com>
Date: Sat, 5 Oct 2024 14:36:58 +0200
Subject: [PATCH] feat: migrate `svelte:self` (#13504)

* feat: migrate `svelte:self`

* chore: regenerate types

* fix: special case `<svelte:self></svelte:self>`

* chore: add special case to tests

* chore: add no filename test

* chore: better migration task message

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* chore: make filename an options object to futureproof it

* chore: simplify open tag `svelte:self`

* chore: simplify migration comment test

* chore: generate types

* chore: apply smart suggestion

* chore: changeset

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
---
 .changeset/gold-pens-sell.md                  |  5 ++
 packages/svelte/src/compiler/migrate/index.js | 66 ++++++++++++++++---
 .../svelte-self-name-conflict/input.svelte    | 19 ++++++
 .../svelte-self-name-conflict/output.svelte   | 22 +++++++
 .../svelte-self-skip-filename/_config.js      |  5 ++
 .../svelte-self-skip-filename/input.svelte    |  3 +
 .../svelte-self-skip-filename/output.svelte   |  4 ++
 .../migrate/samples/svelte-self/input.svelte  | 15 +++++
 .../migrate/samples/svelte-self/output.svelte | 21 ++++++
 packages/svelte/tests/migrate/test.ts         |  8 ++-
 packages/svelte/types/index.d.ts              |  4 +-
 .../src/lib/Output/Compiler.js                |  3 +-
 .../src/lib/workers/compiler/index.js         |  4 +-
 13 files changed, 165 insertions(+), 14 deletions(-)
 create mode 100644 .changeset/gold-pens-sell.md
 create mode 100644 packages/svelte/tests/migrate/samples/svelte-self-name-conflict/input.svelte
 create mode 100644 packages/svelte/tests/migrate/samples/svelte-self-name-conflict/output.svelte
 create mode 100644 packages/svelte/tests/migrate/samples/svelte-self-skip-filename/_config.js
 create mode 100644 packages/svelte/tests/migrate/samples/svelte-self-skip-filename/input.svelte
 create mode 100644 packages/svelte/tests/migrate/samples/svelte-self-skip-filename/output.svelte
 create mode 100644 packages/svelte/tests/migrate/samples/svelte-self/input.svelte
 create mode 100644 packages/svelte/tests/migrate/samples/svelte-self/output.svelte

diff --git a/.changeset/gold-pens-sell.md b/.changeset/gold-pens-sell.md
new file mode 100644
index 0000000000..945c795890
--- /dev/null
+++ b/.changeset/gold-pens-sell.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+feat: support migrating `svelte:self`
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 393b34f5b2..e0d8cd55df 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -27,9 +27,10 @@ const style_placeholder = '/*$$__STYLE_CONTENT__$$*/';
  * May throw an error if the code is too complex to migrate automatically.
  *
  * @param {string} source
+ * @param {{filename?: string}} [options]
  * @returns {{ code: string; }}
  */
-export function migrate(source) {
+export function migrate(source, { filename } = {}) {
 	try {
 		// Blank CSS, could contain SCSS or similar that needs a preprocessor.
 		// Since we don't care about CSS in this migration, we'll just ignore it.
@@ -41,7 +42,7 @@ export function migrate(source) {
 		});
 
 		reset_warning_filter(() => false);
-		reset(source, { filename: 'migrate.svelte' });
+		reset(source, { filename: filename ?? 'migrate.svelte' });
 
 		let parsed = parse(source);
 
@@ -68,6 +69,7 @@ export function migrate(source) {
 		let state = {
 			scope: analysis.instance.scope,
 			analysis,
+			filename,
 			str,
 			indent,
 			props: [],
@@ -90,12 +92,14 @@ export function migrate(source) {
 				createBubbler: analysis.root.unique('createBubbler').name,
 				bubble: analysis.root.unique('bubble').name,
 				passive: analysis.root.unique('passive').name,
-				nonpassive: analysis.root.unique('nonpassive').name
+				nonpassive: analysis.root.unique('nonpassive').name,
+				svelte_self: analysis.root.unique('SvelteSelf').name
 			},
 			legacy_imports: new Set(),
 			script_insertions: new Set(),
 			derived_components: new Map(),
-			derived_labeled_statements: new Set()
+			derived_labeled_statements: new Set(),
+			has_svelte_self: false
 		};
 
 		if (parsed.module) {
@@ -122,12 +126,21 @@ export function migrate(source) {
 			state.script_insertions.size > 0 ||
 			state.props.length > 0 ||
 			analysis.uses_rest_props ||
-			analysis.uses_props;
+			analysis.uses_props ||
+			state.has_svelte_self;
 
 		if (!parsed.instance && need_script) {
 			str.appendRight(0, '<script>');
 		}
 
+		if (state.has_svelte_self && filename) {
+			const file = filename.split('/').pop();
+			str.appendRight(
+				insertion_point,
+				`\n${indent}import ${state.names.svelte_self} from './${file}';`
+			);
+		}
+
 		const specifiers = [...state.legacy_imports].map((imported) => {
 			const local = state.names[imported];
 			return imported === local ? imported : `${imported} as ${local}`;
@@ -298,6 +311,7 @@ export function migrate(source) {
  *  scope: Scope;
  *  str: MagicString;
  *  analysis: ComponentAnalysis;
+ *  filename?: string;
  *  indent: string;
  *  props: Array<{ local: string; exported: string; init: string; bindable: boolean; slot_name?: string; optional: boolean; type: string; comment?: string; type_only?: boolean; needs_refine_type?: boolean; }>;
  *  props_insertion_point: number;
@@ -306,8 +320,9 @@ export function migrate(source) {
  * 	names: Record<string, string>;
  * 	legacy_imports: Set<string>;
  * 	script_insertions: Set<string>;
- *  derived_components: Map<string, string>,
- * 	derived_labeled_statements: Set<LabeledStatement>
+ *  derived_components: Map<string, string>;
+ * 	derived_labeled_statements: Set<LabeledStatement>;
+ *  has_svelte_self: boolean;
  * }} State
  */
 
@@ -729,9 +744,44 @@ const template = {
 		}
 		next();
 	},
+	SvelteSelf(node, { state, next }) {
+		const source = state.str.original.substring(node.start, node.end);
+		if (!state.filename) {
+			const indent = guess_indent(source);
+			state.str.prependRight(
+				node.start,
+				`<!-- @migration-task: svelte:self is deprecated, import this Svelte file into itself instead -->\n${indent}`
+			);
+			next();
+			return;
+		}
+		// overwrite the open tag
+		state.str.overwrite(
+			node.start + 1,
+			node.start + 1 + 'svelte:self'.length,
+			`${state.names.svelte_self}`
+		);
+		// if it has a fragment we need to overwrite the closing tag too
+		if (node.fragment.nodes.length > 0) {
+			state.str.overwrite(
+				state.str.original.lastIndexOf('<', node.end) + 2,
+				node.end - 1,
+				`${state.names.svelte_self}`
+			);
+		} else if (!source.endsWith('/>')) {
+			// special case for case `<svelte:self></svelte:self>` it has no fragment but
+			// we still need to overwrite the end tag
+			state.str.overwrite(
+				node.start + source.lastIndexOf('</', node.end) + 2,
+				node.end - 1,
+				`${state.names.svelte_self}`
+			);
+		}
+		state.has_svelte_self = true;
+		next();
+	},
 	SvelteElement(node, { state, path, next }) {
 		migrate_slot_usage(node, path, state);
-
 		if (node.tag.type === 'Literal') {
 			let is_static = true;
 
diff --git a/packages/svelte/tests/migrate/samples/svelte-self-name-conflict/input.svelte b/packages/svelte/tests/migrate/samples/svelte-self-name-conflict/input.svelte
new file mode 100644
index 0000000000..8309282ce4
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/svelte-self-name-conflict/input.svelte
@@ -0,0 +1,19 @@
+<script>
+	let SvelteSelf;
+</script>
+
+{#if false}
+	<svelte:self />
+	<svelte:self with_attributes/>
+	<svelte:self count={count+1}/>
+	<svelte:self>
+		child
+	</svelte:self>
+	<svelte:self count={count+1}>
+		child
+	</svelte:self>
+	<svelte:self count={$$props.count}     >
+		child
+	</svelte:self>
+	<svelte:self></svelte:self>
+{/if}
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/svelte-self-name-conflict/output.svelte b/packages/svelte/tests/migrate/samples/svelte-self-name-conflict/output.svelte
new file mode 100644
index 0000000000..827d06c3a3
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/svelte-self-name-conflict/output.svelte
@@ -0,0 +1,22 @@
+<script>
+	import SvelteSelf_1 from './output.svelte';
+	/** @type {Record<string, any>} */
+	let { ...props } = $props();
+	let SvelteSelf;
+</script>
+
+{#if false}
+	<SvelteSelf_1 />
+	<SvelteSelf_1 with_attributes/>
+	<SvelteSelf_1 count={count+1}/>
+	<SvelteSelf_1>
+		child
+	</SvelteSelf_1>
+	<SvelteSelf_1 count={count+1}>
+		child
+	</SvelteSelf_1>
+	<SvelteSelf_1 count={props.count}     >
+		child
+	</SvelteSelf_1>
+	<SvelteSelf_1></SvelteSelf_1>
+{/if}
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/svelte-self-skip-filename/_config.js b/packages/svelte/tests/migrate/samples/svelte-self-skip-filename/_config.js
new file mode 100644
index 0000000000..9c8f6f9c78
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/svelte-self-skip-filename/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+	skip_filename: true
+});
diff --git a/packages/svelte/tests/migrate/samples/svelte-self-skip-filename/input.svelte b/packages/svelte/tests/migrate/samples/svelte-self-skip-filename/input.svelte
new file mode 100644
index 0000000000..340b257fc1
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/svelte-self-skip-filename/input.svelte
@@ -0,0 +1,3 @@
+{#if false}
+	<svelte:self />
+{/if}
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/svelte-self-skip-filename/output.svelte b/packages/svelte/tests/migrate/samples/svelte-self-skip-filename/output.svelte
new file mode 100644
index 0000000000..59ae5ff19c
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/svelte-self-skip-filename/output.svelte
@@ -0,0 +1,4 @@
+{#if false}
+	<!-- @migration-task: svelte:self is deprecated, import this Svelte file into itself instead -->
+	<svelte:self />
+{/if}
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/svelte-self/input.svelte b/packages/svelte/tests/migrate/samples/svelte-self/input.svelte
new file mode 100644
index 0000000000..09eb5767be
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/svelte-self/input.svelte
@@ -0,0 +1,15 @@
+{#if false}
+	<svelte:self />
+	<svelte:self with_attributes/>
+	<svelte:self count={count+1}/>
+	<svelte:self>
+		child
+	</svelte:self>
+	<svelte:self count={count+1}>
+		child
+	</svelte:self>
+	<svelte:self count={$$props.count}     >
+		child
+	</svelte:self>
+	<svelte:self></svelte:self>
+{/if}
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/samples/svelte-self/output.svelte b/packages/svelte/tests/migrate/samples/svelte-self/output.svelte
new file mode 100644
index 0000000000..7e7ebae215
--- /dev/null
+++ b/packages/svelte/tests/migrate/samples/svelte-self/output.svelte
@@ -0,0 +1,21 @@
+<script>
+	import SvelteSelf from './output.svelte';
+	/** @type {Record<string, any>} */
+	let { ...props } = $props();
+</script>
+
+{#if false}
+	<SvelteSelf />
+	<SvelteSelf with_attributes/>
+	<SvelteSelf count={count+1}/>
+	<SvelteSelf>
+		child
+	</SvelteSelf>
+	<SvelteSelf count={count+1}>
+		child
+	</SvelteSelf>
+	<SvelteSelf count={props.count}     >
+		child
+	</SvelteSelf>
+	<SvelteSelf></SvelteSelf>
+{/if}
\ No newline at end of file
diff --git a/packages/svelte/tests/migrate/test.ts b/packages/svelte/tests/migrate/test.ts
index 5aa86a194f..e26e8e376b 100644
--- a/packages/svelte/tests/migrate/test.ts
+++ b/packages/svelte/tests/migrate/test.ts
@@ -4,7 +4,9 @@ import { migrate } from 'svelte/compiler';
 import { try_read_file } from '../helpers.js';
 import { suite, type BaseTest } from '../suite.js';
 
-interface ParserTest extends BaseTest {}
+interface ParserTest extends BaseTest {
+	skip_filename?: boolean;
+}
 
 const { test, run } = suite<ParserTest>(async (config, cwd) => {
 	const input = fs
@@ -12,7 +14,9 @@ const { test, run } = suite<ParserTest>(async (config, cwd) => {
 		.replace(/\s+$/, '')
 		.replace(/\r/g, '');
 
-	const actual = migrate(input).code;
+	const actual = migrate(input, {
+		filename: config.skip_filename ? undefined : `${cwd}/output.svelte`
+	}).code;
 
 	// run `UPDATE_SNAPSHOTS=true pnpm test migrate` to update parser tests
 	if (process.env.UPDATE_SNAPSHOTS || !fs.existsSync(`${cwd}/output.svelte`)) {
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 22b47d35ec..e64abef0c5 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1264,7 +1264,9 @@ declare module 'svelte/compiler' {
 	 * May throw an error if the code is too complex to migrate automatically.
 	 *
 	 * */
-	export function migrate(source: string): {
+	export function migrate(source: string, { filename }?: {
+		filename?: string;
+	} | undefined): {
 		code: string;
 	};
 	namespace Css {
diff --git a/sites/svelte-5-preview/src/lib/Output/Compiler.js b/sites/svelte-5-preview/src/lib/Output/Compiler.js
index ae36368241..94484ecf39 100644
--- a/sites/svelte-5-preview/src/lib/Output/Compiler.js
+++ b/sites/svelte-5-preview/src/lib/Output/Compiler.js
@@ -80,7 +80,8 @@ export default class Compiler {
 			this.worker.postMessage({
 				id,
 				type: 'migrate',
-				source: file.source
+				source: file.source,
+				filename: `${file.name}.${file.type}`
 			});
 		});
 	}
diff --git a/sites/svelte-5-preview/src/lib/workers/compiler/index.js b/sites/svelte-5-preview/src/lib/workers/compiler/index.js
index 46853c3984..9247894dd6 100644
--- a/sites/svelte-5-preview/src/lib/workers/compiler/index.js
+++ b/sites/svelte-5-preview/src/lib/workers/compiler/index.js
@@ -133,9 +133,9 @@ function compile({ id, source, options, return_ast }) {
 }
 
 /** @param {import("../workers").MigrateMessageData} param0 */
-function migrate({ id, source }) {
+function migrate({ id, source, filename }) {
 	try {
-		const result = svelte.migrate(source);
+		const result = svelte.migrate(source, { filename });
 
 		return {
 			id,