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,