Skip to content

Commit

Permalink
feat: support migration of svelte:component (#13437)
Browse files Browse the repository at this point in the history
* feat: allow migration of  `svelte:component`

* chore: simplify a lot (thanks @dummdidumm)

* chore: update output

* chore: use `next()` and `snip` instead of walking the AST

* fix: migrate nested `svelte:component`

* Update .changeset/good-vans-bake.md

---------

Co-authored-by: Simon H <[email protected]>
  • Loading branch information
paoloricciuti and dummdidumm authored Sep 30, 2024
1 parent 33ee958 commit b665425
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/good-vans-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: support migration of `svelte:component`
81 changes: 78 additions & 3 deletions packages/svelte/src/compiler/migrate/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
/** @import { VariableDeclarator, Node, Identifier } from 'estree' */
/** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */
/** @import { Scope } from '../phases/scope.js' */
/** @import { Scope, ScopeRoot } from '../phases/scope.js' */
/** @import { AST, Binding, SvelteNode, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { parse } from '../phases/1-parse/index.js';
import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js';
import { reset, reset_warning_filter } from '../state.js';
import { extract_identifiers } from '../utils/ast.js';
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
import { determine_slot } from '../utils/slot.js';
import { validate_component_options } from '../validate-options.js';

const regex_style_tags = /(<style[^>]+>)([\S\s]*?)(<\/style>)/g;
Expand Down Expand Up @@ -85,7 +87,8 @@ export function migrate(source) {
nonpassive: analysis.root.unique('nonpassive').name
},
legacy_imports: new Set(),
script_insertions: new Set()
script_insertions: new Set(),
derived_components: new Map()
};

if (parsed.module) {
Expand All @@ -108,6 +111,7 @@ export function migrate(source) {

const need_script =
state.legacy_imports.size > 0 ||
state.derived_components.size > 0 ||
state.script_insertions.size > 0 ||
state.props.length > 0 ||
analysis.uses_rest_props ||
Expand Down Expand Up @@ -250,6 +254,17 @@ export function migrate(source) {
}
}

insertion_point = parsed.instance
? /** @type {number} */ (parsed.instance.content.end)
: insertion_point;

if (state.derived_components.size > 0) {
str.appendRight(
insertion_point,
`\n${indent}${[...state.derived_components.entries()].map(([init, name]) => `const ${name} = $derived(${init});`).join(`\n${indent}`)}\n`
);
}

if (!parsed.instance && need_script) {
str.appendRight(insertion_point, '\n</script>\n\n');
}
Expand All @@ -273,7 +288,8 @@ export function migrate(source) {
* end: number;
* names: Record<string, string>;
* legacy_imports: Set<string>;
* script_insertions: Set<string>
* script_insertions: Set<string>;
* derived_components: Map<string, string>
* }} State
*/

Expand Down Expand Up @@ -586,6 +602,65 @@ const template = {
handle_events(node, state);
next();
},
SvelteComponent(node, { state, next, path }) {
next();

let expression = state.str
.snip(
/** @type {number} */ (node.expression.start),
/** @type {number} */ (node.expression.end)
)
.toString();

if (
(node.expression.type !== 'Identifier' && node.expression.type !== 'MemberExpression') ||
!regex_valid_component_name.test(expression)
) {
let current_expression = expression;
expression = state.scope.generate('SvelteComponent');
let needs_derived = true;
for (let i = path.length - 1; i >= 0; i--) {
const part = path[i];
if (
part.type === 'EachBlock' ||
part.type === 'AwaitBlock' ||
part.type === 'IfBlock' ||
part.type === 'KeyBlock' ||
part.type === 'SnippetBlock' ||
part.type === 'Component' ||
part.type === 'SvelteComponent'
) {
const indent = state.str.original.substring(
state.str.original.lastIndexOf('\n', node.start) + 1,
node.start
);
state.str.prependLeft(
node.start,
`{@const ${expression} = ${current_expression}}\n${indent}`
);
needs_derived = false;
continue;
}
}
if (needs_derived) {
if (state.derived_components.has(current_expression)) {
expression = /** @type {string} */ (state.derived_components.get(current_expression));
} else {
state.derived_components.set(current_expression, expression);
}
}
}

state.str.overwrite(node.start + 1, node.start + node.name.length + 1, expression);

if (state.str.original.substring(node.end - node.name.length - 1, node.end - 1) === node.name) {
state.str.overwrite(node.end - node.name.length - 1, node.end - 1, expression);
}
let this_pos = state.str.original.lastIndexOf('this', node.expression.start);
while (!state.str.original.charAt(this_pos - 1).trim()) this_pos--;
const end_pos = state.str.original.indexOf('}', node.expression.end) + 1;
state.str.remove(this_pos, end_pos);
},
SvelteWindow(node, { state, next }) {
handle_events(node, state);
next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
const regex_valid_element_name =
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
const regex_valid_component_name =
export const regex_valid_component_name =
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
// (must start with uppercase letter if no dots, can contain dots)
/^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ export function SvelteComponent(node, context) {
w.svelte_component_deprecated(node);
}

context.visit(node.expression);

visit_component(node, context);
}
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/phases/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (node.expression) {
for (const id of extract_identifiers_from_destructuring(node.expression)) {
const binding = scope.declare(id, 'template', 'const');
scope.reference(id, [context.path[context.path.length - 1], node]);
bindings.push(binding);
}
} else {
Expand All @@ -402,6 +403,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
end: node.end
};
const binding = scope.declare(id, 'template', 'const');
scope.reference(id, [context.path[context.path.length - 1], node]);
bindings.push(binding);
}
},
Expand Down
125 changes: 125 additions & 0 deletions packages/svelte/tests/migrate/samples/svelte-component/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<script>
let Component;
let fallback;
</script>

<Component let:Comp>
<svelte:component this={Comp} />
</Component>

<Component let:comp>
<svelte:component this={comp} />
</Component>

<Component let:comp={stuff}>
<svelte:component this={stuff} />
</Component>

<Component>
<div slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</div>
</Component>

<Component>
<svelte:fragment slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:fragment>
</Component>

<Component>
<svelte:element this={"div"} slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:element>
</Component>

<svelte:component this={Component} let:Comp>
<svelte:component this={Comp} />
</svelte:component>

<svelte:component this={Component} let:comp>
<svelte:component this={comp} />
</svelte:component>

<svelte:component this={Component} let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:component>

<svelte:component this={Component}>
<div slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</div>
</svelte:component>

<svelte:component this={Component}>
<svelte:fragment slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:fragment>
</svelte:component>

<svelte:component this={Component}>
<svelte:element this={"div"} slot="x" let:comp={stuff}>
<svelte:component this={stuff} />
</svelte:element>
</svelte:component>

<svelte:component this={Component} />
<svelte:component this={Component} prop value="" on:click on:click={()=>''} />
<svelte:component this={Math.random() > .5 ? $$restProps.heads : $$restProps.tail} prop value="" on:click on:click={()=>''}/>

<svelte:component
this={Component}
prop value=""
on:click
on:click={()=>''}
/>

<svelte:component
this={Math.random() > .5 ? $$restProps.heads : $$restProps.tail}
prop value=""
on:click
on:click={()=>''}
/>

{#if true}
{@const x = {Component}}
<svelte:component this={x['Component']} />
{/if}

{#if true}
{@const x = {Component}}
<svelte:component this={x.Component} />
{/if}

{#each [] as component}
<svelte:component this={component} />
{/each}

{#each [] as Component}
<svelte:component this={Component} />
{/each}

{#each [] as component}
{@const Comp = component.component}
<svelte:component this={Comp} />
{/each}

{#each [] as component}
{@const comp = component.component}
<svelte:component this={comp} />
{/each}

{#await Promise.resolve()}
<svelte:component this={Component} />
<svelte:component this={fallback} />
{:then something}
<svelte:component this={something} />
{:catch e}
<svelte:component this={e} />
{/await}

{#await Promise.resolve() then Something}
<svelte:component this={Something} />
{:catch Error}
<svelte:component this={Error} />
{/await}
Loading

0 comments on commit b665425

Please sign in to comment.