diff --git a/.changeset/dedupe-hoisted-templates.md b/.changeset/dedupe-hoisted-templates.md new file mode 100644 index 0000000000..a91882f525 --- /dev/null +++ b/.changeset/dedupe-hoisted-templates.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: deduplicate identical hoisted templates within a component diff --git a/.changeset/four-loops-agree.md b/.changeset/four-loops-agree.md new file mode 100644 index 0000000000..46d30e8464 --- /dev/null +++ b/.changeset/four-loops-agree.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow declarations in the template diff --git a/.changeset/hoist-rest-excludes.md b/.changeset/hoist-rest-excludes.md new file mode 100644 index 0000000000..52efb06092 --- /dev/null +++ b/.changeset/hoist-rest-excludes.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: hoist `rest_props` exclude list as a module-scope `Set` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f72b6c23e..7eff1b9fe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: Lint: permissions: {} runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 @@ -99,7 +99,7 @@ jobs: if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); } - name: check browser-support docs page is up to date - run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/05-browser-support.md`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/05-browser-support.md; exit 1); }' + run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/.generated/`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/.generated/; exit 1); }' Benchmarks: permissions: {} runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index c6cd3ea310..7f143248aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,8 @@ This guide is for AI coding agents working in the Svelte monorepo. **Important:** Read and follow [`CONTRIBUTING.md`](./CONTRIBUTING.md) as well - it contains essential information about testing, code structure, and contribution guidelines that applies here. +When submitting a PR, you **MUST** read [`PULL_REQUEST_TEMPLATE.md`](./.github/PULL_REQUEST_TEMPLATE.md) and fill it out correctly. **DO NOT** submit a PR without running the full test suite. + ## Quick Reference If asked to do a performance investigation, use the `performance-investigation` skill. diff --git a/documentation/docs/03-template-syntax/10-@const.md b/documentation/docs/03-template-syntax/10-@const.md index 2a587b7a3d..6f2edc1a37 100644 --- a/documentation/docs/03-template-syntax/10-@const.md +++ b/documentation/docs/03-template-syntax/10-@const.md @@ -2,6 +2,8 @@ title: {@const ...} --- +> [!NOTE] `{@const x = y}` is legacy syntax — use [`{const x = $derived(y)}`](declaration-tags) instead + The `{@const ...}` tag defines a local constant. ```svelte diff --git a/documentation/docs/03-template-syntax/11-declaration-tags.md b/documentation/docs/03-template-syntax/11-declaration-tags.md new file mode 100644 index 0000000000..e0edaf6a38 --- /dev/null +++ b/documentation/docs/03-template-syntax/11-declaration-tags.md @@ -0,0 +1,72 @@ +--- +title: {let/const ...} +--- + +Declaration tags define local variables inside markup with `const` or `let`: + + +```svelte + + + +{#each boxes as box} + {const area = box.width * box.height} + {const label = `${box.width} ⨉ ${box.height} = ${area}`} + +

{label}

+{/each} +``` + + +> [!NOTE] Declaration tags are available since Svelte 5.56. + +> [!NOTE] The [`{@const ...}`](@const) syntax is considered legacy — use declaration tags instead. + +When values should be reactive, you can use `$state` and `$derived`: + + +```svelte + + + +

Hello {user.name}

+ + +{#if editing} + {let name = $state(user.name)} + {const greeting = $derived(`Hello ${name}`)} + +
+ +

{greeting}

+ + +{/if} +``` + + +Declaration tags can be used anywhere inside the component. They can reference values declared outside themselves (for example in the ` + + + {const sync = 'sync'} + {const number = await Promise.resolve(5)} + {const after_async =number + 1} + {const { length, 0: first } = await '01234'} + + {#snippet greet()} + {const greeting = $derived(await `Hello, ${name}!`)} +

{greeting}

+ {number} + {#if number > 4 && after_async && greeting} + {const length = $derived(await number)} + {#each { length }, index} + {const i = $derived(await index)} + {i} + {/each} + {/if} + {/snippet} + + {@render greet()} + {number} {sync} {after_async} {length} {first} + + {#if sync} + {const double = $derived(number * 2)} + {double} + {/if} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js new file mode 100644 index 0000000000..8fd2fc0976 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [top, change] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

Hello name

+
nested Hi name
+ ` + ); + + top.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

Hello name

+
nested Hi name
+ ` + ); + + change.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

Hello other

+
nested Hi other
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte new file mode 100644 index 0000000000..4521ea2e41 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte @@ -0,0 +1,20 @@ + + +{let name = $state(top_id)} + + +{#if id} + {let name = $state(await id)} + {let greeting = $derived(await `Hello ${name}`)} + + +

{greeting}

+
+ {const nested = 'nested'} + {const greeting2 = $derived(await `Hi ${name}`)} + {nested} {greeting2} +
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/_config.js new file mode 100644 index 0000000000..901df42df7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: '', + async test({ assert, target }) { + const [increment] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte new file mode 100644 index 0000000000..da72bc69c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte @@ -0,0 +1,3 @@ +{let count = $state(0)} +{let doubled = $derived(count * 2)} + diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js new file mode 100644 index 0000000000..54e390ec83 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

4 total

nested
nested
`, + async test({ assert, target }) { + const [top, toggle, increment] = target.querySelectorAll('button'); + + top.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

4 total

nested
nested
` + ); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

6 total

nested
nested
` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `
nested
` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

4 total

nested
nested
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte new file mode 100644 index 0000000000..5953104092 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte @@ -0,0 +1,29 @@ + + +{let top = $state(1)} +{let top_doubled = $derived(top * 2)} + + + + +{#if visible} + {let counter = $state({ value: initial })} + {let doubled = $derived(counter.value * 2)} + {const suffix = ' total'} + {const format = (value) => `${value}${suffix}`} + + +

{format(doubled)}

+
+ {const doubled = 'nested'} + {doubled} +
+{/if} + +
+ {const nested = 'nested'} + {nested} +
diff --git a/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js index 8bd7e97780..5c1d27ae54 100644 --- a/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/client'; -var root_1 = $.from_html(`

`); +var root = $.from_html(`

`); export default function Async_const($$anchor) { var fragment = $.comment(); @@ -18,7 +18,7 @@ export default function Async_const($$anchor) { () => b = $.derived(() => $.get(a) + 1) ]); - var p = root_1(); + var p = root(); var text = $.child(p, true); $.reset(p); diff --git a/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/client/index.svelte.js new file mode 100644 index 0000000000..d0d474fa29 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/client/index.svelte.js @@ -0,0 +1,44 @@ +import 'svelte/internal/disclose-version'; +import * as $ from 'svelte/internal/client'; + +var root = $.from_html(`

hello

`); +var root_1 = $.from_html(` `, 1); + +export default function Dedupe_templates($$anchor, $$props) { + var fragment = root_1(); + var node = $.first_child(fragment); + + { + var consequent = ($$anchor) => { + var p = root(); + + $.append($$anchor, p); + }; + + var alternate = ($$anchor) => { + var p_1 = root(); + + $.append($$anchor, p_1); + }; + + $.if(node, ($$render) => { + if ($$props.a) $$render(consequent); else $$render(alternate, -1); + }); + } + + var node_1 = $.sibling(node, 2); + + { + var consequent_1 = ($$anchor) => { + var p_2 = root(); + + $.append($$anchor, p_2); + }; + + $.if(node_1, ($$render) => { + if ($$props.b) $$render(consequent_1); + }); + } + + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js new file mode 100644 index 0000000000..1f4a56779e --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js @@ -0,0 +1,24 @@ +import * as $ from 'svelte/internal/server'; + +export default function Dedupe_templates($$renderer, $$props) { + let { a, b } = $$props; + + if (a) { + $$renderer.push(''); + $$renderer.push(`

hello

`); + } else { + $$renderer.push(''); + $$renderer.push(`

hello

`); + } + + $$renderer.push(` `); + + if (b) { + $$renderer.push(''); + $$renderer.push(`

hello

`); + } else { + $$renderer.push(''); + } + + $$renderer.push(``); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dedupe-templates/index.svelte b/packages/svelte/tests/snapshot/samples/dedupe-templates/index.svelte new file mode 100644 index 0000000000..c75b5481a5 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/dedupe-templates/index.svelte @@ -0,0 +1,14 @@ + + + +{#if a} +

hello

+{:else} +

hello

+{/if} + +{#if b} +

hello

+{/if} diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js index 43f2eadc09..d2f2f87f20 100644 --- a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js @@ -2,14 +2,14 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root_1 = $.from_html(``); +var root = $.from_html(``); export default function Delegated_locally_declared_shadowed($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); $.each(node, 0, () => ({ length: 1 }), $.index, ($$anchor, $$item, index) => { - var button = root_1(); + var button = root(); $.set_attribute(button, 'data-index', index); diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js index 804a7c26f1..049d47a96f 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -2,14 +2,14 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root_1 = $.from_html(`

`); +var root = $.from_html(`

`); export default function Each_index_non_null($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => { - var p = root_1(); + var p = root(); p.textContent = `index: ${i}`; $.append($$anchor, p); diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js index 5a46b9bbef..7df616f694 100644 --- a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js @@ -1,10 +1,12 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; +var rest_excludes = new Set(['$$slots', '$$events', '$$legacy']); + export default function Props_identifier($$anchor, $$props) { $.push($$props, true); - let props = $.rest_props($$props, ['$$slots', '$$events', '$$legacy']); + let props = $.rest_props($$props, rest_excludes); $$props.a; props[a]; diff --git a/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js index 8f8b115d70..c8354fe67e 100644 --- a/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js @@ -4,63 +4,51 @@ import * as $ from 'svelte/internal/client'; import Option from './Option.svelte'; const opt = ($$anchor) => { - var option = root_1(); + var option = root(); $.append($$anchor, option); }; const option_snippet = ($$anchor) => { - var option_1 = root_2(); + var option_1 = root_1(); $.append($$anchor, option_1); }; const option_snippet2 = ($$anchor) => { - var option_2 = root_3(); + var option_2 = root_2(); $.append($$anchor, option_2); }; const conditional_option = ($$anchor) => { - var option_3 = root_4(); + var option_3 = root_3(); $.append($$anchor, option_3); }; -var root_1 = $.from_html(``); -var root_2 = $.from_html(``); -var root_3 = $.from_html(``); -var root_4 = $.from_html(``); +var root = $.from_html(``); +var root_1 = $.from_html(``); +var root_2 = $.from_html(``); +var root_3 = $.from_html(``); var option_content = $.from_html(`Rich`, 1); -var root_5 = $.from_html(``); -var root_6 = $.from_html(``); -var root_7 = $.from_html(``); +var root_4 = $.from_html(``); +var root_5 = $.from_html(``); +var root_6 = $.from_html(``); var select_content = $.from_html(``, 1); -var root_8 = $.from_html(``); var option_content_1 = $.from_html(`Bold`, 1); -var root_9 = $.from_html(``); var option_content_2 = $.from_html(`Italic text`, 1); var option_content_3 = $.from_html(` `, 1); -var root_10 = $.from_html(``); -var root_12 = $.from_html(``); -var root_13 = $.from_html(``); +var root_7 = $.from_html(``); +var root_8 = $.from_html(``); var option_content_4 = $.from_html(`Rich in boundary`, 1); -var root_14 = $.from_html(``); -var select_content_1 = $.from_html(``, 1); -var select_content_2 = $.from_html(``, 1); -var select_content_3 = $.from_html(``, 1); -var optgroup_content = $.from_html(``, 1); -var optgroup_content_1 = $.from_html(``, 1); -var option_content_5 = $.from_html(``, 1); -var select_content_4 = $.from_html(``, 1); -var select_content_5 = $.from_html(``, 1); -var root = $.from_html(` `, 1); +var root_9 = $.from_html(` `, 1); export default function Select_with_rich_content($$anchor) { let items = [1, 2, 3]; let show = true; let html = ''; - var fragment = root(); + var fragment = root_9(); var select = $.first_child(fragment); var option_4 = $.child(select); @@ -76,7 +64,7 @@ export default function Select_with_rich_content($$anchor) { var select_1 = $.sibling(select, 2); $.each(select_1, 5, () => items, $.index, ($$anchor, item) => { - var option_5 = root_5(); + var option_5 = root_4(); var text = $.child(option_5, true); $.reset(option_5); @@ -101,7 +89,7 @@ export default function Select_with_rich_content($$anchor) { { var consequent = ($$anchor) => { - var option_6 = root_6(); + var option_6 = root_5(); $.append($$anchor, option_6); }; @@ -117,7 +105,7 @@ export default function Select_with_rich_content($$anchor) { var node_1 = $.child(select_3); $.key(node_1, () => items, ($$anchor) => { - var option_7 = root_7(); + var option_7 = root_6(); $.append($$anchor, option_7); }); @@ -139,7 +127,7 @@ export default function Select_with_rich_content($$anchor) { $.each(select_5, 5, () => items, $.index, ($$anchor, item) => { const x = $.derived_safe_equal(() => $.get(item) * 2); - var option_8 = root_8(); + var option_8 = root_4(); var text_1 = $.child(option_8, true); $.reset(option_8); @@ -177,7 +165,7 @@ export default function Select_with_rich_content($$anchor) { var optgroup_1 = $.child(select_7); $.each(optgroup_1, 5, () => items, $.index, ($$anchor, item) => { - var option_10 = root_9(); + var option_10 = root_4(); var text_2 = $.child(option_10, true); $.reset(option_10); @@ -215,7 +203,7 @@ export default function Select_with_rich_content($$anchor) { var select_9 = $.sibling(select_8, 2); $.each(select_9, 5, () => items, $.index, ($$anchor, item) => { - var option_12 = root_10(); + var option_12 = root_7(); $.customizable_select(option_12, () => { var anchor_4 = $.child(option_12); @@ -242,7 +230,7 @@ export default function Select_with_rich_content($$anchor) { var node_4 = $.first_child(fragment_6); $.each(node_4, 1, () => items, $.index, ($$anchor, item) => { - var option_13 = root_12(); + var option_13 = root_4(); var text_4 = $.child(option_13, true); $.reset(option_13); @@ -274,7 +262,7 @@ export default function Select_with_rich_content($$anchor) { var node_5 = $.child(select_11); $.boundary(node_5, {}, ($$anchor) => { - var option_14 = root_13(); + var option_14 = root_8(); $.append($$anchor, option_14); }); @@ -285,7 +273,7 @@ export default function Select_with_rich_content($$anchor) { var node_6 = $.child(select_12); $.boundary(node_6, {}, ($$anchor) => { - var option_15 = root_14(); + var option_15 = root_7(); $.customizable_select(option_15, () => { var anchor_5 = $.child(option_15); @@ -303,7 +291,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_13, () => { var anchor_6 = $.child(select_13); - var fragment_8 = select_content_1(); + var fragment_8 = select_content(); var node_7 = $.first_child(fragment_8); Option(node_7, {}); @@ -314,7 +302,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_14, () => { var anchor_7 = $.child(select_14); - var fragment_9 = select_content_2(); + var fragment_9 = select_content(); var node_8 = $.first_child(fragment_9); option_snippet(node_8); @@ -325,7 +313,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_15, () => { var anchor_8 = $.child(select_15); - var fragment_10 = select_content_3(); + var fragment_10 = select_content(); var node_9 = $.first_child(fragment_10); $.html(node_9, () => html); @@ -337,7 +325,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(optgroup_2, () => { var anchor_9 = $.child(optgroup_2); - var fragment_11 = optgroup_content(); + var fragment_11 = select_content(); var node_10 = $.first_child(fragment_11); Option(node_10, {}); @@ -351,7 +339,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(optgroup_3, () => { var anchor_10 = $.child(optgroup_3); - var fragment_12 = optgroup_content_1(); + var fragment_12 = select_content(); var node_11 = $.first_child(fragment_12); option_snippet2(node_11); @@ -365,7 +353,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(option_16, () => { var anchor_11 = $.child(option_16); - var fragment_13 = option_content_5(); + var fragment_13 = select_content(); var node_12 = $.first_child(fragment_13); $.html(node_12, () => 'Bold HTML'); @@ -378,7 +366,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_19, () => { var anchor_12 = $.child(select_19); - var fragment_14 = select_content_4(); + var fragment_14 = select_content(); var node_13 = $.first_child(fragment_14); $.each(node_13, 1, () => items, $.index, ($$anchor, item) => { @@ -392,7 +380,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_20, () => { var anchor_13 = $.child(select_20); - var fragment_16 = select_content_5(); + var fragment_16 = select_content(); var node_14 = $.first_child(fragment_16); { diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.svelte new file mode 100644 index 0000000000..8cfdf59c0f --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.svelte @@ -0,0 +1,3 @@ +{#if true} + {function foo() {}} +{/if} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json new file mode 100644 index 0000000000..052636cddb --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_type", + "message": "Declaration tags must be `let` or `const` declarations", + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 14, + "column": 8 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte new file mode 100644 index 0000000000..129a8ac7ba --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte @@ -0,0 +1,15 @@ +{#if true} + + {type} + {type } + {type && foo} + {type || bar} + {type % 2} + {type .x} + {type ?.x} + {type ()} + {type [1]} + {type `tag`} + + {type foo = boolean} +{/if} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json new file mode 100644 index 0000000000..2a9b3c0140 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_type", + "message": "Declaration tags must be `let` or `const` declarations", + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 5 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte new file mode 100644 index 0000000000..eb9fcd5e75 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte @@ -0,0 +1,3 @@ +{#if true} + {var foo = 1} +{/if} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/errors.json new file mode 100644 index 0000000000..6b89f2eab8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_no_legacy_mode", + "message": "Declaration tags cannot be used in legacy mode", + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 19 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte new file mode 100644 index 0000000000..026d4eff1e --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte @@ -0,0 +1,5 @@ + + +{const foo = 'foo'} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/input.svelte new file mode 100644 index 0000000000..081f242a81 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/input.svelte @@ -0,0 +1,6 @@ + + +Usage when no explicit runes/legacy mode should be ok +{const foo = world} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 3f71d44177..1c3795be80 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1303,6 +1303,12 @@ declare module 'svelte/compiler' { }; } + /** A `{let ...}` or `{const ...}` tag */ + export interface DeclarationTag extends BaseNode { + type: 'DeclarationTag'; + declaration: VariableDeclaration; + } + /** A `{@debug ...}` tag */ export interface DebugTag extends BaseNode { type: 'DebugTag'; @@ -1613,6 +1619,7 @@ declare module 'svelte/compiler' { export type Tag = | AST.AttachTag | AST.ConstTag + | AST.DeclarationTag | AST.DebugTag | AST.ExpressionTag | AST.HtmlTag diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c077f0f6b..767af0515a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: 25.0.1 version: 25.0.1 playwright: - specifier: ^1.58.0 - version: 1.58.0 + specifier: ^1.60.0 + version: 1.60.0 prettier: specifier: ^3.2.4 version: 3.2.4 @@ -121,8 +121,8 @@ importers: specifier: ^0.3.25 version: 0.3.31 '@playwright/test': - specifier: ^1.58.0 - version: 1.58.0 + specifier: ^1.60.0 + version: 1.60.0 '@rollup/plugin-commonjs': specifier: ^28.0.1 version: 28.0.1(rollup@4.60.1) @@ -864,8 +864,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.58.0': - resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} engines: {node: '>=18'} hasBin: true @@ -1096,6 +1096,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1459,8 +1462,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -1699,8 +1702,8 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2162,13 +2165,13 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - playwright-core@1.58.0: - resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} hasBin: true - playwright@1.58.0: - resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==} + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} engines: {node: '>=18'} hasBin: true @@ -2307,6 +2310,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -2403,8 +2411,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} term-size@2.2.1: @@ -3283,9 +3291,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.58.0': + '@playwright/test@1.60.0': dependencies: - playwright: 1.58.0 + playwright: 1.60.0 '@polka/url@1.0.0-next.25': {} @@ -3463,13 +3471,15 @@ snapshots: '@types/eslint@8.56.12': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/esrecurse@4.3.1': {} '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/json-schema@7.0.15': {} '@types/node@12.20.55': {} @@ -3840,10 +3850,10 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.20.1: + enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.2 + tapable: 2.3.3 enquirer@2.4.1: dependencies: @@ -3945,7 +3955,7 @@ snapshots: eslint-compat-utils@0.5.1(eslint@10.0.0): dependencies: eslint: 10.0.0 - semver: 7.7.4 + semver: 7.8.1 eslint-config-prettier@9.1.0(eslint@10.0.0): dependencies: @@ -3965,14 +3975,14 @@ snapshots: eslint-plugin-n@17.24.0(eslint@10.0.0)(typescript@5.5.4): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) - enhanced-resolve: 5.20.1 + enhanced-resolve: 5.22.1 eslint: 10.0.0 eslint-plugin-es-x: 7.8.0(eslint@10.0.0) - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 - semver: 7.7.4 + semver: 7.8.1 ts-declaration-location: 1.0.7(typescript@5.5.4) transitivePeerDependencies: - typescript @@ -4176,7 +4186,7 @@ snapshots: function-bind@1.1.2: {} - get-tsconfig@4.13.7: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -4596,11 +4606,11 @@ snapshots: pify@4.0.1: {} - playwright-core@1.58.0: {} + playwright-core@1.60.0: {} - playwright@1.58.0: + playwright@1.60.0: dependencies: - playwright-core: 1.58.0 + playwright-core: 1.60.0 optionalDependencies: fsevents: 2.3.2 @@ -4742,6 +4752,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.1: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -4829,7 +4841,7 @@ snapshots: symbol-tree@3.2.4: {} - tapable@2.3.2: {} + tapable@2.3.3: {} term-size@2.2.1: {}