Merge remote-tracking branch 'origin/main' into svelte-custom-renderer

pull/18359/head
paoloricciuti 2 months ago
commit a72c3650c9

@ -1,5 +0,0 @@
---
'svelte': patch
---
perf: use `createElement` instead of `createElementNS` for HTML elements

@ -1,5 +0,0 @@
---
'svelte': patch
---
perf: store `current_sources` as a `Set` for O(1) membership checks

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly transform references to earlier declarators in a declaration tag (e.g. `{let a = $state(0), b = $derived(a * 2)}`)

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid spurious `state_referenced_locally` warnings for `$derived` declarations in declaration tags

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: tolerate whitespace before `let`/`const` in declaration tags

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent infinite loop when a tag's expression ends with a trailing `/` at the end of the input

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: more robust parsing of declaration tags with regards to `type`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve newlines in spread input values when the `type` attribute is applied after `value`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: update `SvelteURLSearchParams` when setting duplicate keys to the same joined value

@ -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

@ -8,6 +8,7 @@ packages/**/config/*.js
# packages/svelte
packages/svelte/messages/**/*.md
packages/svelte/scripts/_bundle.js
packages/svelte/scripts/_baseline/*.ts
packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.js
packages/svelte/src/internal/client/errors.js

@ -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.

@ -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

@ -0,0 +1,72 @@
---
title: {let/const ...}
---
Declaration tags define local variables inside markup with `const` or `let`:
<!-- codeblock:start {"title":"Declaration tags"} -->
```svelte
<!--- file: App.svelte --->
<script>
let boxes = [{ width: 10, height: 10 }, { width: 15, height: 15 }];
</script>
{#each boxes as box}
{const area = box.width * box.height}
{const label = `${box.width} ⨉ ${box.height} = ${area}`}
<p>{label}</p>
{/each}
```
<!-- codeblock:end -->
> [!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`:
<!-- codeblock:start {"title":"Reactive declaration tags"} -->
```svelte
<!--- file: App.svelte --->
<script>
let user = $state({ name: 'Svelte' });
let editing = $state(false);
</script>
<p>Hello {user.name}</p>
<button onclick={() => editing = true}>edit name</button>
{#if editing}
{let name = $state(user.name)}
{const greeting = $derived(`Hello ${name}`)}
<hr>
<input bind:value={name} />
<p>{greeting}</p>
<button onclick={() => {
user.name = name;
editing = false;
}}>save</button>
{/if}
```
<!-- codeblock:end -->
Declaration tags can be used anywhere inside the component. They can reference values declared outside themselves (for example in the `<script>` tag or in `{#each ...}` blocks) and are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
<!-- codeblock:start {"title":"Declaration tag scope"} -->
```svelte
<!--- file: App.svelte --->
{const hello = 'hello'}
{hello} <!-- 'hello' -->
<div>
{const hello = 'hi'}
{hello} <!-- 'hi' -->
<div>
{hello} <!-- 'hi' -->
</div>
</div>
{hello} <!-- 'hello' -->
```
<!-- codeblock:end -->

@ -1,7 +1,7 @@
<!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit -->
| Feature | Chrome/Edge | Firefox | Safari |
| --- | ---: | ---: | ---: |
| `$state.snapshot` | 98 | 94 | 15.4 |
| `bind:devicePixelContentBoxSize` | <span style="color: var(--sk-fg-4)"></span> | 93 | not supported |
| `flip` from `svelte/animate` | <span style="color: var(--sk-fg-4)"></span> | 126 | <span style="color: var(--sk-fg-4)"></span> |
| - | - | - | - |
| [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) | 98 | 94 | 15.4 |
| [`bind:devicePixelContentBoxSize`](/docs/svelte/bind#Dimensions) | <span style="color: var(--sk-fg-4)"></span> | 93 | not supported |
| [`flip` from `svelte/animate`](/docs/svelte/svelte-animate#flip) | <span style="color: var(--sk-fg-4)"></span> | 126 | <span style="color: var(--sk-fg-4)"></span> |

@ -1,13 +1,15 @@
<!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit -->
| Browser | Minimum version |
| ---------------- | --------------- |
| Chrome/Edge | 87 |
| Firefox | 83 |
| Safari | 14 |
| Opera | 73 |
| Opera (Android) | 62 |
| Samsung Internet | 14.0 |
| Android WebView | 87 |
| Browser | Minimum version |
| - | - |
| Chrome/Edge | 87 |
| Firefox | 83 |
| Safari | 14 |
| Opera | 73 |
| Opera (Android) | 62 |
| Samsung Internet | 14.0 |
| Android WebView | 87 |
| Internet Explorer | not supported |
> [!NOTE] This equates to a <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> target of 2020.

@ -2,29 +2,14 @@
title: Browser support
---
The table below shows the minimum browser versions Svelte's runtime and compiled output are expected to work in.
The table below shows the minimum browser versions Svelte is expected to work in, derived from the browser APIs used by Svelte's internal code.
@include .generated/browser-support.md
These numbers describe what Svelte's output _requires_ in order to run — they're derived from the APIs the code uses, not from a list of browsers the team commits to testing.
This table only covers Svelte itself. It does not include [SvelteKit](/docs/kit), other Svelte libraries, or your own code.
## What is covered
## Exceptions
- **Svelte's runtime.** Everything you import from `svelte` or its subpackages, in the form your bundler ships to the browser.
- **Compiler output.** The JavaScript the Svelte compiler emits from your `.svelte` files, including the DOM operations, bindings and transitions used in your components.
## What is not covered
- **Your own code** inside `<script>` blocks or `.svelte.js` files. If you use newer browser APIs the table will not reflect them — configure your own [browserslist](https://github.com/browserslist/browserslist) and polyfills accordingly.
- **SvelteKit**, adapters and build tooling. See the [SvelteKit docs](https://svelte.dev/docs/kit) for the browser support story there.
- **Internet Explorer 11.** Svelte's runtime relies on `Proxy`, which cannot be polyfilled. IE11 is not supported and there is no path to making it work.
## Per-feature browser requirements
Some Svelte features rely on browser APIs that exceed the floor above. The runtime still loads on older browsers — modern bundlers tree-shake the affected code when the feature is unused — but if you use one of these features, you need the higher minimum version listed here.
A few Svelte features require a higher minimum browser version. You'll only need to take the following table into consideration if you use these specific features.
@include .generated/browser-support-features.md
## How this page stays accurate
The minimum versions can only move forward in a minor or major release, and any change is recorded in the [changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md). Every Svelte feature — bindings, runes, directives, and module exports — is checked against the [web-features](https://www.npmjs.com/package/web-features) Baseline dataset on every pull request, and the build fails if a change requires newer browsers than the page reflects.

@ -399,6 +399,18 @@ Invalid selector
Cannot declare a variable with the same name as an import from `<script module>`
```
### declaration_tag_invalid_type
```
Declaration tags must be `let` or `const` declarations
```
### declaration_tag_no_legacy_mode
```
Declaration tags cannot be used in legacy mode
```
### derived_invalid_export
```

@ -37,7 +37,7 @@
"eslint-plugin-lube": "^0.5.1",
"eslint-plugin-svelte": "^3.15.0",
"jsdom": "25.0.1",
"playwright": "^1.58.0",
"playwright": "^1.60.0",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",

@ -1,5 +1,21 @@
# svelte
## 5.56.0
### Minor Changes
- feat: allow declarations in the template ([#18282](https://github.com/sveltejs/svelte/pull/18282))
### Patch Changes
- perf: use `createElement` instead of `createElementNS` for HTML elements ([#18262](https://github.com/sveltejs/svelte/pull/18262))
- perf: store `current_sources` as a `Set` for O(1) membership checks ([#18278](https://github.com/sveltejs/svelte/pull/18278))
- perf: deduplicate identical hoisted templates within a component ([#18320](https://github.com/sveltejs/svelte/pull/18320))
- perf: hoist `rest_props` exclude list as a module-scope `Set` ([#18252](https://github.com/sveltejs/svelte/pull/18252))
## 5.55.10
### Patch Changes

@ -191,6 +191,14 @@ The same applies to components:
> {@debug ...} arguments must be identifiers, not arbitrary expressions
## declaration_tag_invalid_type
> Declaration tags must be `let` or `const` declarations
## declaration_tag_no_legacy_mode
> Declaration tags cannot be used in legacy mode
## directive_invalid_value
> Directive value must be a JavaScript expression enclosed in curly braces

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.55.10",
"version": "5.56.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -159,7 +159,7 @@
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.58.0",
"@playwright/test": "^1.60.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
@ -169,12 +169,12 @@
"baseline-browser-mapping": "^2.10.32",
"dts-buddy": "^0.5.5",
"esbuild": "^0.25.10",
"web-features": "^3.29.0",
"rollup": "^4.59.0",
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",
"typescript": "^5.5.4",
"vitest": "^2.1.9"
"vitest": "^2.1.9",
"web-features": "^3.29.0"
},
"dependencies": {
"@jridgewell/remapping": "^2.3.4",

@ -1,3 +1,4 @@
/* eslint-disable no-console */
// Regenerates `documentation/docs/07-misc/05-browser-support.md`.
//
// Pipeline:
@ -46,10 +47,17 @@ type BrowserVersions = Record<string, string | null>;
type RuntimeFloor = number | 'newly';
type ConditionalRow = {
name: string;
doc_link: string | null;
versions: BrowserVersions;
baseline_year: RuntimeFloor;
};
const doc_links: Record<string, string | null> = {
'`$state.snapshot`': '/docs/svelte/$state#$state.snapshot',
'`bind:devicePixelContentBoxSize`': '/docs/svelte/bind#Dimensions',
'`flip` from `svelte/animate`': '/docs/svelte/svelte-animate#flip'
};
// Supplemental detection rules for APIs `web-features` doesn't track
// yet. Each rule is checked with full TS type-aware precision — the
// only reason it lives here instead of being auto-derived is that no
@ -125,13 +133,7 @@ const SAFE_TO_IGNORE = new Set(['devicepixelratio', 'trusted-types']);
/**
* Suppressions for features that DO live in the runtime but are reached
* only via a specific code path documented in the per-feature table on
* the docs page. The aggregate scan hides them so the headline floor
* reflects "load Svelte and use the basic runtime", not "use every
* conditional feature".
*
* Each entry MUST appear in the conditional-features table. The
* staleness check below also verifies the entry is still present in the
* runtime if the detector doesn't flag it, the entry can be removed.
* the docs page.
*/
const BEHAVIORAL_IGNORE = new Set([
'structured-clone',
@ -203,7 +205,6 @@ async function enumerate_subpackage_exports(): Promise<Record<string, string[]>>
if (names.length > 0) result[module_id] = names;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
// eslint-disable-next-line no-console
console.warn(` (could not enumerate ${module_id}: ${message.split('\n')[0]})`);
}
}
@ -437,10 +438,8 @@ function find_minimum_target(
// detected feature happens to lack a Baseline year.
const final_year = Math.max(year, 2015);
// eslint-disable-next-line no-console
console.log(`${final_year} (features that drove the floor:)`);
for (const id of [...drivers].sort()) {
// eslint-disable-next-line no-console
console.log(` - ${id}`);
}
@ -455,7 +454,6 @@ function find_minimum_target(
*
* `SAFE_TO_IGNORE` entries are exempt: they're safe to carry regardless
* of whether the runtime currently uses the API.
*
*/
function validate_ignore_features(runtime_files: string[]): void {
if (BEHAVIORAL_IGNORE.size === 0) return;
@ -528,14 +526,12 @@ function enumerate_features(subpackage_exports: Record<string, string[]>): Featu
* Produce the `.svelte` source for a single binding fixture. Returns
* `null` for bindings the compiler treats as elements rather than
* properties (none currently, but defensive).
*
*/
function binding_fixture(name: string, props: BindingProperty): string {
// Map declared `valid_elements` to a concrete element + minimal attrs
// so the compiler accepts the binding.
const tag = (props.valid_elements ?? ['div'])[0];
// Pick a sensible initial value and attribute set per binding.
const reactive = `let v = $state();`;
if (tag === 'svelte:window') {
@ -545,7 +541,7 @@ function binding_fixture(name: string, props: BindingProperty): string {
return `<script>${reactive}</script><svelte:document bind:${name}={v} />`;
}
if (tag === 'input') {
// `bind:checked` requires type="checkbox"|"radio"; `bind:group` too.
// `bind:checked` and `bind:group` require type="checkbox" | "radio"
const type =
name === 'checked' || name === 'indeterminate'
? ' type="checkbox"'
@ -567,7 +563,6 @@ function binding_fixture(name: string, props: BindingProperty): string {
* Compile a `.svelte` fixture to JS (no-op for `.js` fixtures), then
* bundle the result through the shared `bundle` helper. Fixtures are tiny
* so circular-dep warnings from the Svelte runtime are silenced.
*
*/
async function bundle_fixture(feature: Feature): Promise<string> {
const entry_code =
@ -612,6 +607,8 @@ async function find_all_conditional_features(
const features = enumerate_features(subpackage_exports);
const rows: ConditionalRow[] = [];
const missing_doc_links: string[] = [];
for (let i = 0; i < features.length; i++) {
const feature = features[i];
process.stdout.write(`\r ${i + 1}/${features.length} ${feature.name}`.padEnd(80));
@ -645,25 +642,35 @@ async function find_all_conditional_features(
}
}
let doc_link = doc_links[feature.name];
if (doc_link === undefined) {
doc_link = null;
missing_doc_links.push(feature.name);
}
rows.push({
name: feature.name,
doc_link,
versions,
baseline_year: final_year
});
}
process.stdout.write('\n');
if (missing_doc_links.length) {
throw new Error(`Missing documentation url for some features.
Add them to the \`doc_links\` map in \`scripts/generate-browser-support.ts\`, or add an explicit \`null\` if they don't have a documentation url.
${missing_doc_links.map((name) => ` - "${name}"`).join('\n')}`);
}
return rows;
}
/**
* Render the per-feature browser-requirements table from the auto-detected
* rows. Sorted by Safari floor descending, then alphabetically.
*/
function render_conditional_table(rows: ConditionalRow[], runtime_floor: RuntimeFloor): string {
if (rows.length === 0) {
function render_conditional_table(features: ConditionalRow[], runtime_floor: RuntimeFloor): string {
if (features.length === 0) {
return '_No features currently require browser versions newer than the runtime floor._';
}
features.sort((a, b) => a.name.localeCompare(b.name));
const browsers = [
['chrome', 'Chrome/Edge'],
@ -671,32 +678,25 @@ function render_conditional_table(rows: ConditionalRow[], runtime_floor: Runtime
['safari', 'Safari']
] as const;
const runtime_versions = browser_versions_for(runtime_floor);
const sorted = [...rows].sort((a, b) => {
const sa = Number(a.versions.safari ?? '0');
const sb = Number(b.versions.safari ?? '0');
if (sb !== sa) return sb - sa;
return a.name.localeCompare(b.name);
});
const header = '| Feature | ' + browsers.map(([, label]) => `${label}`).join(' | ') + ' |';
const sep = '| --- |' + browsers.map(() => ' ---: |').join('');
const floor_versions = browser_versions_for(runtime_floor);
const body = sorted.map((entry) => {
const cells = browsers.map(([key]) => {
const v = entry.versions[key];
const rows: string[][] = [];
for (const row of features) {
const name_cell = row.doc_link ? `[${row.name}](${row.doc_link})` : row.name;
const versions = browsers.map(([key]) => {
const v = row.versions[key];
if (v === null) return 'not supported';
if (v === undefined) return '<span style="color: var(--sk-fg-4)">—</span>';
const floor_v = runtime_versions[key];
return floor_v && Number(v) <= Number(floor_v)
? '<span style="color: var(--sk-fg-4)">—</span>'
: v;
const floor_v = floor_versions[key];
if (floor_v && Number(v) <= Number(floor_v))
return '<span style="color: var(--sk-fg-4)">—</span>';
return v;
});
return `| ${entry.name} | ${cells.join(' | ')} |`;
});
rows.push([name_cell, ...versions]);
}
return [header, sep, ...body].join('\n');
return render_markdown_table(['Feature', ...browsers.map(([, label]) => `${label}`)], rows);
}
function browser_versions_for(target: RuntimeFloor): Record<string, string> {
@ -750,53 +750,59 @@ function browser_versions_for(target: RuntimeFloor): Record<string, string> {
return lookup;
}
function render_table(versions: Record<string, string>, target: RuntimeFloor): string {
// Chrome and Edge ship from the same engine and historically resolve to the
// same Baseline version. Collapse them into one row when they match, but
// fall back to listing them separately if they ever drift.
const chrome_edge: [string, string] | null =
versions.chrome && versions.chrome === versions.edge ? ['Chrome/Edge', versions.chrome] : null;
const base_rows: Array<[string, string]> = chrome_edge
? [chrome_edge, ['Chrome (Android)', versions.chrome_android]]
: [
['Chrome', versions.chrome],
['Chrome (Android)', versions.chrome_android],
['Edge', versions.edge]
];
const rows = [
...base_rows,
['Firefox', versions.firefox],
['Firefox (Android)', versions.firefox_android],
['Safari', versions.safari],
['Safari (iOS)', versions.safari_ios],
['Opera', versions.opera],
['Opera (Android)', versions.opera_android],
['Samsung Internet', versions.samsunginternet_android],
['Android WebView', versions.webview_android]
].filter(([label, version]) => version !== undefined) as Array<[string, string]>;
const headings = ['Browser', 'Minimum version'];
const widths = headings.map((heading, i) =>
Math.max(heading.length, ...rows.map((r) => String(r[i]).length))
);
const BROWSER = {
chrome: 'Chrome',
edge: 'Edge',
firefox: 'Firefox',
safari: 'Safari',
opera: 'Opera',
samsung_internet: 'Samsung Internet',
webview_android: 'Android WebView',
internet_explorer: 'Internet Explorer'
};
const pad = (s: string, n: number) => s + ' '.repeat(Math.max(0, n - s.length));
function render_browser_table(versions: Record<string, string>, target: RuntimeFloor): string {
const rows: Array<[string, string]> = [
[BROWSER.chrome, versions.chrome],
[`${BROWSER.chrome} (Android)`, versions.chrome_android]
];
const header = `| ${headings.map((heading, i) => pad(heading, widths[i])).join(' | ')} |`;
const sep = `| ${widths.map((width) => '-'.repeat(width)).join(' | ')} |`;
const body = rows
.map(([a, b]) => `| ${pad(a, widths[0])} | ${pad(String(b), widths[1])} |`)
.join('\n');
if (versions.chrome === versions.edge) {
rows[0][0] += `/${BROWSER.edge}`;
} else {
rows.push([BROWSER.edge, versions.edge]);
}
rows.push(
[BROWSER.firefox, versions.firefox],
[`${BROWSER.firefox} (Android)`, versions.firefox_android],
[BROWSER.safari, versions.safari],
[`${BROWSER.safari} (iOS)`, versions.safari_ios],
[BROWSER.opera, versions.opera],
[`${BROWSER.opera} (Android)`, versions.opera_android],
[BROWSER.samsung_internet, versions.samsunginternet_android],
[BROWSER.webview_android, versions.webview_android],
[BROWSER.internet_explorer, 'not supported']
);
const target_label = target === 'newly' ? '"newly available"' : target;
return `${header}\n${sep}\n${body}\n\n> [!NOTE] This equates to a <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> target of ${target_label}.`;
return (
render_markdown_table(
['Browser', 'Minimum version'],
rows.filter(([, version]) => version !== undefined)
) +
`\n\n> [!NOTE] This equates to a <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> target of ${target_label}.`
);
}
function render_markdown_table(headers: string[], rows: string[][]): string {
return `| ${headers.join(' | ')} |
| ${headers.map(() => '-').join(' | ')} |
${rows.map((row) => `| ${row.join(' | ')} |`).join('\n')}
`;
}
/* eslint-disable no-console */
async function main() {
console.log('Preparing scratch directory…');
// Wipe and recreate so stale bundles can't leak into the next scan.
@ -844,14 +850,11 @@ async function main() {
const versions = browser_versions_for(target);
console.log('Rewriting docs page…');
generate('browser-support.md', render_table(versions, target));
generate('browser-support.md', render_browser_table(versions, target));
generate('browser-support-features.md', render_conditional_table(conditional_rows, target));
console.log('Done.');
} finally {
// Ensure cleanup happens even on failure — otherwise leftover bundle
// files in `scripts/_baseline/` get picked up by `pnpm lint` on the
// next CI step and produce spurious naming/no-console errors.
fs.rmSync(tmp_dir, { recursive: true, force: true });
}
}

@ -1004,6 +1004,24 @@ export function debug_tag_invalid_arguments(node) {
e(node, 'debug_tag_invalid_arguments', `{@debug ...} arguments must be identifiers, not arbitrary expressions\nhttps://svelte.dev/e/debug_tag_invalid_arguments`);
}
/**
* Declaration tags must be `let` or `const` declarations
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function declaration_tag_invalid_type(node) {
e(node, 'declaration_tag_invalid_type', `Declaration tags must be \`let\` or \`const\` declarations\nhttps://svelte.dev/e/declaration_tag_invalid_type`);
}
/**
* Declaration tags cannot be used in legacy mode
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function declaration_tag_no_legacy_mode(node) {
e(node, 'declaration_tag_no_legacy_mode', `Declaration tags cannot be used in legacy mode\nhttps://svelte.dev/e/declaration_tag_no_legacy_mode`);
}
/**
* Directive value must be a JavaScript expression enclosed in curly braces
* @param {null | number | NodeLike} node

@ -262,6 +262,10 @@ export function convert(source, ast) {
};
},
// @ts-ignore
DeclarationTag(node) {
return node;
},
// @ts-ignore
KeyBlock(node, { visit }) {
remove_surrounding_whitespace_nodes(node.fragment.nodes);
return {

@ -1,10 +1,11 @@
/** @import { Comment, Program } from 'estree' */
/** @import { Comment, Program, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from './index.js' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import * as e from '../../errors.js';
import { find_matching_bracket } from './utils/bracket.js';
const JSParser = acorn.Parser;
const TSParser = JSParser.extend(tsPlugin());
@ -98,6 +99,48 @@ export function parse_expression_at(parser, source, index) {
}
}
/**
* @param {Parser} parser
* @param {string} source
* @param {number} index
* @returns {Statement}
*/
export function parse_statement_at(parser, source, index) {
const acorn = parser.ts ? TSParser : JSParser;
let end = find_matching_bracket(source, index, '{');
if (end === undefined) e.unexpected_eof(source.length);
while (source[end - 1] === ';') {
end -= 1;
}
const padded_source = `${' '.repeat(index)}${source.slice(index, end)}`;
const { onComment, add_comments } = get_comment_handlers(
padded_source,
parser.root.comments,
index
);
try {
const ast = acorn.parse(padded_source, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
const statement = /** @type {Statement} */ (
/** @type {unknown} */ (/** @type {Program} */ (ast).body[0])
);
statement.end = Math.min(/** @type {number} */ (statement.end), end);
return statement;
} catch (e) {
handle_parse_error(e);
}
}
const regex_position_indicator = / \(\d+:\d+\)$/;
/**

@ -302,7 +302,10 @@ export class Parser {
}
pop() {
this.fragments.pop();
const fragment = this.fragments.pop();
if (fragment?.metadata.transparent && fragment.nodes.some((n) => n.type === 'DeclarationTag')) {
fragment.metadata.transparent = false;
}
return this.stack.pop();
}

@ -1,16 +1,20 @@
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { ExpressionMetadata } from '../../nodes.js';
import { parse_expression_at } from '../acorn.js';
import { parse_expression_at, parse_statement_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
import { match_bracket } from '../utils/bracket.js';
import { find_matching_bracket, match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /\s*}/y;
const regex_supported_declaration = /(?:let|const)\b/y;
const regex_unsupported_declaration = /(?:var|interface|enum)\b/y;
// `type` is a contextual keyword; this is just a shape hint, confirmed by parsing.
const regex_maybe_type_declaration = /type\b/y;
const pointy_bois = { '<': '>' };
@ -31,6 +35,20 @@ export default function tag(parser) {
}
}
const declaration = read_declaration(parser);
if (declaration) {
parser.append({
type: 'DeclarationTag',
start,
end: parser.index,
declaration: /** @type {VariableDeclaration} */ (declaration),
metadata: {
expression: new ExpressionMetadata()
}
});
return;
}
const expression = read_expression(parser);
parser.allow_whitespace();
@ -47,6 +65,89 @@ export default function tag(parser) {
});
}
/**
* @param {Parser} parser
* @returns {null | import('estree').VariableDeclaration}
*/
function read_declaration(parser) {
const start = parser.index;
const unsupported = parser.match_regex(regex_unsupported_declaration);
if (unsupported) {
e.declaration_tag_invalid_type({ start, end: start + unsupported.length });
}
if (
!parser.match_regex(regex_supported_declaration) &&
// `type` is special, since it is not a reserved keyword and can be used
// as part of a valid expression. We gotta parse first and then see what it is.
!parser.match_regex(regex_maybe_type_declaration)
) {
return null;
}
const initial_comment_count = parser.root.comments.length;
/** @type {import('estree').Statement | import('estree').VariableDeclaration} */
let declaration;
try {
declaration = parse_statement_at(parser, parser.template, start);
} catch (error) {
if (!parser.loose) throw error;
const end = find_matching_bracket(parser.template, start, '{');
if (end === undefined) throw error;
parser.index = end;
const kind = parser.template.startsWith('const', start) ? 'const' : 'let';
declaration = {
type: 'VariableDeclaration',
kind,
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: '',
start: parser.index,
end: parser.index
},
init: null,
start: parser.index,
end: parser.index
}
],
start,
end
};
}
if (declaration.type !== 'VariableDeclaration') {
if (declaration.type === 'ExpressionStatement') {
parser.root.comments.length = initial_comment_count; // Else they show up duplicated
return null;
} else {
// This is a TSTypeAliasDeclaration
e.declaration_tag_invalid_type({
start: declaration.start ?? start,
end: declaration.end ?? parser.index
});
}
}
// TODO support using
if (declaration.kind !== 'let' && declaration.kind !== 'const') {
e.declaration_tag_invalid_type(declaration);
}
parser.index = /** @type {number} */ (declaration.end);
parser.allow_whitespace();
parser.eat('}', true);
return declaration;
}
/** @param {Parser} parser */
function open(parser) {
let start = parser.index - 2;

@ -39,7 +39,9 @@ function find_string_end(string, search_start_index, string_start_char) {
* @returns {number} The index of the end of this regex expression, or `Infinity` if not found.
*/
function find_regex_end(string, search_start_index) {
return find_unescaped_char(string, search_start_index, '/');
const slash = find_unescaped_char(string, search_start_index, '/');
const eol = find_unescaped_char(string, search_start_index, '\n');
return slash < eol ? slash : Infinity;
}
/**
@ -105,7 +107,11 @@ export function find_matching_bracket(template, index, open) {
continue;
case '/': {
const next_char = template[i + 1];
if (!next_char) continue;
if (!next_char) {
// `/` is the last character; advance past it so we don't loop forever
i++;
continue;
}
if (next_char === '/') {
i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length;
continue;
@ -114,7 +120,12 @@ export function find_matching_bracket(template, index, open) {
i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length;
continue;
}
i = find_regex_end(template, i + 1) + '/'.length;
const end = find_regex_end(template, i + 1) + '/'.length;
if (end === Infinity) {
i++;
} else {
i = end;
}
continue;
}
default: {

@ -36,6 +36,7 @@ import { ClassDeclaration } from './visitors/ClassDeclaration.js';
import { ClassDirective } from './visitors/ClassDirective.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DeclarationTag } from './visitors/DeclarationTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportDefaultDeclaration } from './visitors/ExportDefaultDeclaration.js';
@ -157,6 +158,7 @@ const visitors = {
ClassDirective,
Component,
ConstTag,
DeclarationTag,
DebugTag,
EachBlock,
ExportDefaultDeclaration,
@ -312,6 +314,7 @@ export function analyze_module(source, options) {
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
parent_element: null,
in_declaration_tag: false,
reactive_statement: null,
derived_function_depth: -1
},
@ -725,6 +728,7 @@ export function analyze_component(root, source, options) {
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
fragment: ast === template.ast ? ast : null,
parent_element: null,
in_declaration_tag: false,
has_props_rune: false,
component_slots: new Set(),
expression: null,
@ -792,6 +796,7 @@ export function analyze_component(root, source, options) {
options,
fragment: ast === template.ast ? ast : null,
parent_element: null,
in_declaration_tag: false,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
reactive_statement: null,

@ -16,6 +16,8 @@ export interface AnalysisState {
* Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.
*/
parent_element: string | null;
/** True if inside DeclarationTag */
in_declaration_tag: boolean;
has_props_rune: boolean;
/** Which slots the current parent component has */
component_slots: Set<string>;
@ -35,7 +37,7 @@ export interface AnalysisState {
*/
derived_function_depth: number;
/** Collected info about async `{@const }` declarations */
/** Collected info about async `{@const }`/`{let/const ...}` declarations */
async_consts?: {
id: Identifier;
/** How many `$.run(...)` entries are already allocated in this scope */

@ -255,6 +255,9 @@ export function CallExpression(node, context) {
if (expression.has_await) {
context.state.analysis.async_deriveds.add(node);
}
// Tell surrounding declaration tag about metadata for correct calculation of blockers etc
if (context.state.in_declaration_tag) context.state.expression?.merge(expression);
} else if (rune === '$inspect') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
} else {

@ -1,8 +1,8 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import * as b from '#compiler/builders';
import { validate_opening_tag } from './shared/utils.js';
import { mark_async_declaration } from './DeclarationTag.js';
/**
* @param {AST.ConstTag} node
@ -44,28 +44,5 @@ export function ConstTag(node, context) {
derived_function_depth: context.state.function_depth + 1
});
const has_await = node.metadata.expression.has_await;
const blockers = [...node.metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (has_await || context.state.async_consts || blockers.length > 0) {
const run = (context.state.async_consts ??= {
id: context.state.analysis.root.unique('promises'),
declaration_count: 0
});
node.metadata.promises_id = run.id;
const bindings = context.state.scope.get_bindings(declaration);
// keep the counter in sync with the number of thunks pushed in ConstTag in transform
// TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust
// via something like the approach in https://github.com/sveltejs/svelte/pull/18032
const length = run.declaration_count + (blockers.length > 0 ? 1 : 0);
run.declaration_count += blockers.length > 0 ? 2 : 1;
const blocker = b.member(run.id, b.literal(length), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
}
mark_async_declaration(context, node.metadata, [declaration]);
}

@ -0,0 +1,60 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as b from '#compiler/builders';
import * as e from '../../../errors.js';
/**
* @param {AST.DeclarationTag} node
* @param {Context} context
*/
export function DeclarationTag(node, context) {
if (!context.state.analysis.runes && !context.state.analysis.maybe_runes) {
e.declaration_tag_no_legacy_mode(node);
}
context.visit(node.declaration, {
...context.state,
in_declaration_tag: true,
// the declaration lives in the fragment scope, which is one level deeper than the
// `function_depth` we're tracking here (`set_scope` doesn't update `function_depth`).
// align them so that `state_referenced_locally` warnings are calculated correctly
function_depth: context.state.scope.function_depth,
expression: node.metadata.expression
});
mark_async_declaration(context, node.metadata, node.declaration.declarations);
}
/**
* @param {Context} context
* @param {AST.ConstTag['metadata'] | AST.DeclarationTag['metadata']} metadata
* @param {import('estree').VariableDeclarator[]} declarations
*/
export function mark_async_declaration(context, metadata, declarations) {
const has_await = metadata.expression.has_await;
const blockers = [...metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (has_await || context.state.async_consts || blockers.length > 0) {
const run = (context.state.async_consts ??= {
id: context.state.analysis.root.unique('promises'),
declaration_count: 0
});
metadata.promises_id = run.id;
const bindings = declarations.flatMap((declaration) =>
context.state.scope.get_bindings(declaration)
);
// keep the counter in sync with the number of thunks pushed in transform
// TODO 6.0 once non-async and non-runes mode is gone investigate making this more robust
// via something like the approach in https://github.com/sveltejs/svelte/pull/18032
const length = run.declaration_count + (blockers.length > 0 ? 1 : 0);
run.declaration_count += blockers.length > 0 ? 2 : 1;
const blocker = b.member(run.id, b.literal(length), true);
for (const binding of bindings) {
binding.blocker = blocker;
}
}
}

@ -162,7 +162,7 @@ export function Identifier(node, context) {
if (binding.metadata?.is_template_declaration && context.state.options.experimental.async) {
let snippet_name;
// Find out if this references a {@const ...} declaration of an implicit children snippet
// Find out if this references a {@const ...}/{let/const ...} declaration of an implicit children snippet
// when it is itself inside a snippet block at the same level. If so, error.
for (let i = context.path.length - 1; i >= 0; i--) {
const parent = context.path[i];

@ -4,7 +4,7 @@
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
import * as b from '#compiler/builders';
import { build_getter, is_state_source } from './utils.js';
import { build_getter, get_transform } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { dev, filename, custom_renderer } from '../../../state.js';
import { AnimateDirective } from './visitors/AnimateDirective.js';
@ -22,6 +22,7 @@ import { ClassBody } from './visitors/ClassBody.js';
import { Comment } from './visitors/Comment.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DeclarationTag } from './visitors/DeclarationTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
@ -66,20 +67,7 @@ const visitors = {
const scope = state.scopes.get(node);
if (scope && scope !== state.scope) {
const transform = { ...state.transform };
for (const [name, binding] of scope.declarations) {
if (
binding.kind === 'normal' ||
// Reads of `$state(...)` declarations are not
// transformed if they are never reassigned
(binding.kind === 'state' && !is_state_source(binding, state.analysis))
) {
delete transform[name];
}
}
next({ ...state, transform, scope });
next({ ...state, transform: get_transform(scope, state), scope });
} else {
next();
}
@ -99,6 +87,7 @@ const visitors = {
Comment,
Component,
ConstTag,
DeclarationTag,
DebugTag,
EachBlock,
ExportNamedDeclaration,
@ -152,6 +141,7 @@ export function client_component(analysis, options) {
scopes: analysis.module.scopes,
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted],
templates: new Map(),
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),

@ -1,3 +1,4 @@
/** @import { TemplateLiteral } from 'estree' */
/** @import { Namespace } from '#compiler' */
/** @import { ComponentClientTransformState } from '../types.js' */
/** @import { Node } from './types.js' */
@ -31,15 +32,30 @@ function build_locations(nodes) {
/**
* @param {ComponentClientTransformState} state
* @param {Namespace} namespace
* @param {string} name
* @param {number} [flags]
*/
export function transform_template(state, namespace, flags = 0) {
export function transform_template(state, name, flags = 0) {
const namespace = state.metadata.namespace;
// custom renderers needs a tree to work because there's no template element we can use
const tree = state.options.fragments === 'tree' || custom_renderer;
const expression = tree ? state.template.as_tree() : state.template.as_html();
const key =
tree || dev
? null
: get_template_key(
/** @type {TemplateLiteral} */ (expression),
state.metadata.namespace,
flags
);
if (key !== null) {
const existing = state.templates.get(key);
if (existing !== undefined) return existing;
}
if (tree) {
if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
@ -64,5 +80,26 @@ export function transform_template(state, namespace, flags = 0) {
);
}
return call;
const id = state.scope.root.unique(name);
state.hoisted.push(b.var(id, call));
if (key !== null) {
state.templates.set(key, id);
}
return id;
}
/**
* Returns a stable key for templates that are safe to deduplicate - plain
* `$.from_html`/`from_svg`/`from_mathml` factories with literal arguments - or `null`
* for anything else. Dev-mode templates are wrapped in `$.add_locations(...)`, which
* embeds per-call-site locations, so they never produce a key and are never shared.
* @param {TemplateLiteral} template
* @param {Namespace} namespace
* @param {number} flags
* @returns {string | null}
*/
function get_template_key(template, namespace, flags) {
return `${namespace} ${flags} ${template.quasis[0].value.raw}`;
}

@ -40,6 +40,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly analysis: ComponentAnalysis;
readonly options: ValidatedCompileOptions;
readonly hoisted: Array<Statement | ModuleDeclaration>;
/** Deduplicates hoisted templates by content, mapping a template key to its hoisted identifier */
readonly templates: Map<string, Identifier>;
readonly events: Set<string>;
readonly store_to_invalidate?: string;

@ -179,3 +179,24 @@ export function create_derived(state, expression, async = false) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
}
}
/**
* @param {Scope} scope
* @param {ClientTransformState} state
*/
export function get_transform(scope, state) {
const transform = { ...state.transform };
for (const [name, binding] of scope.declarations) {
if (
binding.kind === 'normal' ||
// Reads of `$state(...)` declarations are not
// transformed if they are never reassigned
(binding.kind === 'state' && !is_state_source(binding, state.analysis))
) {
delete transform[name];
}
}
return transform;
}

@ -7,6 +7,7 @@ import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
import { add_async_declaration } from './DeclarationTag.js';
/**
* @param {AST.ConstTag} node
@ -26,7 +27,7 @@ export function ConstTag(node, context) {
context.state.transform[declaration.id.name] = { read: get_value };
add_const_declaration(context.state, declaration.id, expression, node.metadata);
add_const_declaration(context, declaration.id, expression, node.metadata);
} else {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
@ -63,7 +64,7 @@ export function ConstTag(node, context) {
expression = b.call('$.tag', expression, b.literal('[@const]'));
}
add_const_declaration(context.state, tmp, expression, node.metadata);
add_const_declaration(context, tmp, expression, node.metadata);
for (const node of identifiers) {
context.state.transform[node.name] = {
@ -74,38 +75,26 @@ export function ConstTag(node, context) {
}
/**
* @param {ComponentContext['state']} state
* @param {ComponentContext} context
* @param {Identifier} id
* @param {Expression} expression
* @param {AST.ConstTag['metadata']} metadata
*/
function add_const_declaration(state, id, expression, metadata) {
function add_const_declaration(context, id, expression, metadata) {
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
const after = dev ? [b.stmt(b.call('$.get', id))] : [];
const blockers = [...metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== state.async_consts?.id);
if (metadata.promises_id) {
const run = (state.async_consts ??= {
id: metadata.promises_id,
thunks: []
});
state.consts.push(b.let(id));
if (blockers.length === 1) {
run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
}
// keep the number of thunks pushed in sync with ConstTag in analysis phase
const assignment = b.assignment('=', id, expression);
run.thunks.push(b.thunk(assignment, metadata.expression.has_await));
add_async_declaration(
context,
metadata,
[id],
[b.stmt(b.assignment('=', id, expression))],
'let'
);
} else {
const { state } = context;
state.consts.push(b.const(id, expression));
state.consts.push(...after);
}

@ -0,0 +1,89 @@
/** @import { Expression, Identifier, Pattern, Statement, ExpressionStatement, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { extract_identifiers, has_await_expression } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js';
/**
* @param {AST.DeclarationTag} node
* @param {ComponentContext} context
*/
export function DeclarationTag(node, context) {
// register the transformers _before_ visiting the declaration, so that
// later declarators can reference earlier ones (e.g. `{let a = $state(0), b = $derived(a * 2)}`)
add_state_transformers(context);
const declaration = /** @type {Statement | undefined} */ (context.visit(node.declaration));
if (
node.metadata.promises_id &&
node.declaration.type === 'VariableDeclaration' &&
declaration?.type === 'VariableDeclaration'
) {
const { ids, assignments } = build_async_declaration_parts(declaration);
add_async_declaration(context, node.metadata, ids, assignments, declaration.kind);
} else {
context.state.consts.push(declaration ?? node.declaration);
}
}
/**
* @param {VariableDeclaration} declaration
*/
export function build_async_declaration_parts(declaration) {
const ids = new Map();
for (const declarator of declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
ids.set(id.name, id);
}
}
const assignments = declaration.declarations
.filter((declarator) => declarator.init !== null)
.map((declarator) =>
b.stmt(
b.assignment(
'=',
/** @type {Pattern} */ (declarator.id),
/** @type {Expression} */ (declarator.init)
)
)
);
return { ids: [...ids.values()], assignments };
}
/**
* @param {ComponentContext} context
* @param {AST.ConstTag['metadata'] | AST.DeclarationTag['metadata']} metadata
* @param {Identifier[]} ids
* @param {ExpressionStatement[]} assignments
* @param {VariableDeclaration['kind']} [kind]
*/
export function add_async_declaration(context, metadata, ids, assignments, kind = 'let') {
const run = (context.state.async_consts ??= {
id: /** @type {Identifier} */ (metadata.promises_id),
thunks: []
});
for (const id of ids) {
context.state.consts.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
}
const blockers = [...metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (blockers.length === 1) {
run.thunks.push(b.thunk(b.member(/** @type {Expression} */ (blockers[0]), 'promise')));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('$.wait', b.array(blockers))));
}
// keep the number of thunks pushed in sync with analysis phase
const has_await =
metadata.expression.has_await ||
assignments.some((assignment) => has_await_expression(assignment));
const body = assignments.length === 1 ? assignments[0].expression : b.block(assignments);
run.thunks.push(b.thunk(body, has_await));
}

@ -52,7 +52,6 @@ export function Fragment(node, context) {
(trimmed[0].type === 'IfBlock' &&
trimmed[0].elseif &&
/** @type {AST.IfBlock} */ (parent).metadata.flattened?.includes(trimmed[0])));
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
/** @type {Statement[]} */
const body = [];
@ -96,8 +95,7 @@ export function Fragment(node, context) {
let flags = state.template.needs_import_node ? TEMPLATE_USE_IMPORT_NODE : undefined;
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
const template_name = transform_template(state, 'root', flags);
state.init.unshift(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
@ -147,8 +145,7 @@ export function Fragment(node, context) {
// special case — we can use `$.comment` instead of creating a unique template
state.init.unshift(b.var(id, b.call('$.comment')));
} else {
const template = transform_template(state, namespace, flags);
state.hoisted.push(b.var(template_name, template));
const template_name = transform_template(state, 'root', flags);
state.init.unshift(b.var(id, b.call(template_name)));
}

@ -18,7 +18,7 @@ import {
is_customizable_select_element
} from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js';
import { build_getter, get_transform } from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
@ -303,11 +303,14 @@ export function RegularElement(node, context) {
}
}
const scope = /** @type {Scope} */ (context.state.scopes.get(node.fragment));
/** @type {ComponentClientTransformState} */
const state = {
...context.state,
metadata,
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
scope,
transform: get_transform(scope, context.state),
preserve_whitespace: context.state.preserve_whitespace || name === 'pre' || name === 'textarea'
};
@ -321,8 +324,19 @@ export function RegularElement(node, context) {
state.options.preserveComments
);
const has_declarations = !node.fragment.metadata.transparent;
/** @type {typeof state} */
const child_state = { ...state, init: [], update: [], after_update: [], snippets: [] };
const child_state = {
...state,
init: [],
update: [],
after_update: [],
snippets: [],
consts: has_declarations ? [] : state.consts,
async_consts: has_declarations ? undefined : state.async_consts,
memoizer: has_declarations ? new Memoizer() : state.memoizer
};
for (const node of hoisted) {
context.visit(node, child_state);
@ -379,7 +393,6 @@ export function RegularElement(node, context) {
context.state.template.push_comment();
// Create a separate template for the rich content
const template_name = context.state.scope.root.unique(`${name}_content`);
const fragment_id = b.id(context.state.scope.generate('fragment'));
const anchor_id = b.id(context.state.scope.generate('anchor'));
@ -403,9 +416,8 @@ export function RegularElement(node, context) {
}
);
// Transform the template to $.from_html(...) and hoist it
const template = transform_template(select_state, metadata.namespace, TEMPLATE_FRAGMENT);
context.state.hoisted.push(b.var(template_name, template));
// Transform the template to $.from_html(...) and hoist it (deduplicating identical templates)
const template_name = transform_template(select_state, `${name}_content`, TEMPLATE_FRAGMENT);
// Build the rich content function body
// The anchor is the child of the element (a hydration marker during hydration)
@ -446,11 +458,21 @@ export function RegularElement(node, context) {
}
}
if (node.fragment.nodes.some((node) => node.type === 'SnippetBlock')) {
if (node.fragment.nodes.some((node) => node.type === 'SnippetBlock') || has_declarations) {
if (child_state.async_consts && child_state.async_consts.thunks.length > 0) {
child_state.consts.push(
b.var(
child_state.async_consts.id,
b.call('$.run', b.array(child_state.async_consts.thunks))
)
);
}
// Wrap children in `{...}` to avoid declaration conflicts
context.state.init.push(
b.block([
...child_state.snippets,
...child_state.consts,
...child_state.init,
...element_state.init,
child_state.update.length > 0 ? build_render_statement(child_state) : b.empty,

@ -40,6 +40,7 @@ export function SvelteBoundary(node, context) {
const hoisted = [];
let has_const = false;
let has_declaration = false;
// const tags need to live inside the boundary, but might also be referenced in hoisted snippets.
// to resolve this we cheat: we duplicate const tags inside snippets
@ -55,6 +56,10 @@ export function SvelteBoundary(node, context) {
});
}
}
if (child.type === 'DeclarationTag') {
has_declaration = true;
}
}
for (const child of node.fragment.nodes) {
@ -68,10 +73,10 @@ export function SvelteBoundary(node, context) {
if (child.type === 'SnippetBlock') {
if (
context.state.options.experimental.async &&
has_const &&
(has_const || has_declaration) &&
!['failed', 'pending'].includes(child.expression.name)
) {
// we can't hoist snippets as they may reference const tags, so we just keep them in the fragment
// we can't hoist snippets as they may reference const/declaration tags, so we just keep them in the fragment
nodes.push(child);
} else {
/** @type {Statement[]} */

@ -49,8 +49,13 @@ export function VariableDeclaration(node, context) {
}
if (declarator.id.type === 'Identifier') {
const exclude_id = context.state.scope.root.unique('rest_excludes');
context.state.hoisted.push(
b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name)))))
);
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
const args = [b.id('$$props'), exclude_id];
if (dev) {
// include rest name, so we can provide informative error messages
@ -95,8 +100,13 @@ export function VariableDeclaration(node, context) {
}
} else {
// RestElement
const exclude_id = context.state.scope.root.unique('rest_excludes');
context.state.hoisted.push(
b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name)))))
);
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
const args = [b.id('$$props'), exclude_id];
if (dev) {
// include rest name, so we can provide informative error messages

@ -15,6 +15,7 @@ import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DeclarationTag } from './visitors/DeclarationTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
@ -64,6 +65,7 @@ const template_visitors = {
AwaitBlock,
Component,
ConstTag,
DeclarationTag,
DebugTag,
EachBlock,
Fragment,

@ -28,7 +28,7 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly preserve_whitespace: boolean;
/** True if the current node is a) a component or render tag and b) the sole child of a block */
readonly is_standalone: boolean;
/** Transformed async `{@const }` declarations (if any) and those coming after them */
/** Transformed async `{@const }`/`{let/const ...}` declarations (if any) and those coming after them */
async_consts?: {
id: Identifier;
thunks: Expression[];

@ -1,8 +1,9 @@
/** @import { Expression, Pattern, Statement } from 'estree' */
/** @import { Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { extract_identifiers } from '../../../../utils/ast.js';
import { add_async_declaration } from './DeclarationTag.js';
/**
* @param {AST.ConstTag} node
@ -12,31 +13,15 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
const id = /** @type {Pattern} */ (context.visit(declaration.id));
const init = /** @type {Expression} */ (context.visit(declaration.init));
const blockers = [...node.metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (node.metadata.promises_id) {
const run = (context.state.async_consts ??= {
id: node.metadata.promises_id,
thunks: []
});
const identifiers = extract_identifiers(declaration.id);
for (const identifier of identifiers) {
context.state.init.push(b.let(identifier.name));
}
if (blockers.length === 1) {
run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0])));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
}
// keep the number of thunks pushed in sync with ConstTag in analysis phase
const assignment = b.assignment('=', id, init);
run.thunks.push(b.thunk(assignment, node.metadata.expression.has_await));
add_async_declaration(
context,
node.metadata,
extract_identifiers(id),
[b.stmt(b.assignment('=', id, init))],
'let'
);
} else {
context.state.init.push(b.const(id, init));
}

@ -0,0 +1,85 @@
/** @import { Expression, Identifier, Pattern, Statement, ExpressionStatement, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { extract_identifiers, has_await_expression } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
/**
* @param {AST.DeclarationTag} node
* @param {ComponentContext} context
*/
export function DeclarationTag(node, context) {
const declaration = /** @type {Statement} */ (context.visit(node.declaration));
if (
node.metadata.promises_id &&
node.declaration.type === 'VariableDeclaration' &&
declaration.type === 'VariableDeclaration'
) {
const { ids, assignments } = build_async_declaration_parts(declaration);
add_async_declaration(context, node.metadata, ids, assignments, declaration.kind);
} else {
context.state.init.push(declaration);
}
}
/**
* @param {VariableDeclaration} declaration
*/
export function build_async_declaration_parts(declaration) {
const ids = new Map();
for (const declarator of declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
ids.set(id.name, id);
}
}
const assignments = declaration.declarations
.filter((declarator) => declarator.init !== null)
.map((declarator) =>
b.stmt(
b.assignment(
'=',
/** @type {Pattern} */ (declarator.id),
/** @type {Expression} */ (declarator.init)
)
)
);
return { ids: [...ids.values()], assignments };
}
/**
* @param {ComponentContext} context
* @param {AST.ConstTag['metadata'] | AST.DeclarationTag['metadata']} metadata
* @param {Identifier[]} ids
* @param {ExpressionStatement[]} assignments
* @param {VariableDeclaration['kind']} [kind]
*/
export function add_async_declaration(context, metadata, ids, assignments, kind = 'let') {
const run = (context.state.async_consts ??= {
id: /** @type {Identifier} */ (metadata.promises_id),
thunks: []
});
for (const id of ids) {
context.state.init.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
}
const blockers = [...metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (blockers.length === 1) {
run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0])));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
}
// keep the number of thunks pushed in sync with analysis phase
const has_await =
metadata.expression.has_await ||
assignments.some((assignment) => has_await_expression(assignment));
const body = assignments.length === 1 ? assignments[0].expression : b.block(assignments);
run.thunks.push(b.thunk(body, has_await));
}

@ -17,17 +17,23 @@ import { is_customizable_select_element } from '../../../nodes.js';
export function RegularElement(node, context) {
const name = context.state.namespace === 'html' ? node.name.toLowerCase() : node.name;
const namespace = determine_namespace_for_children(node, context.state.namespace);
const has_child_declarations = !node.fragment.metadata.transparent;
/** @type {ComponentServerTransformState} */
const state = {
...context.state,
namespace,
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea',
init: [],
template: []
template: [],
async_consts: undefined
};
/** @type {ComponentServerTransformState} */
const attribute_state = { ...state, scope: context.state.scope };
const node_is_void = is_void(name);
const optimiser = new PromiseOptimiser();
@ -50,7 +56,11 @@ export function RegularElement(node, context) {
if (!is_special) {
// only open the tag in the non-special path
state.template.push(b.literal(`<${name}`));
body = build_element_attributes(node, { ...context, state }, optimiser.transform);
body = build_element_attributes(
node,
{ ...context, state: attribute_state },
optimiser.transform
);
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
}
@ -72,10 +82,7 @@ export function RegularElement(node, context) {
node.fragment.nodes,
context.path,
namespace,
{
...state,
scope: /** @type {Scope} */ (state.scopes.get(node.fragment))
},
state,
state.preserve_whitespace,
state.options.preserveComments
);
@ -209,7 +216,17 @@ export function RegularElement(node, context) {
state.template.push(b.stmt(b.call('$.pop_element')));
}
if (optimiser.is_async()) {
if (has_child_declarations && state.async_consts && state.async_consts.thunks.length > 0) {
state.init.push(
b.var(state.async_consts.id, b.call('$$renderer.run', b.array(state.async_consts.thunks)))
);
}
if (has_child_declarations) {
context.state.template.push(
...optimiser.render([b.block([...state.init, ...build_template(state.template)])])
);
} else if (optimiser.is_async()) {
context.state.template.push(
...optimiser.render([...state.init, ...build_template(state.template)])
);

@ -152,6 +152,7 @@ export function clean_nodes(
if (
node.type === 'ConstTag' ||
node.type === 'DeclarationTag' ||
node.type === 'DebugTag' ||
node.type === 'SvelteBody' ||
node.type === 'SvelteWindow' ||

@ -217,6 +217,7 @@ function* find_descendants(fragment) {
case 'SnippetBlock':
case 'DebugTag':
case 'ConstTag':
case 'DeclarationTag':
case 'Comment':
case 'ExpressionTag':
break;

@ -603,6 +603,48 @@ const svelte_visitors = (comments) => ({
context.write('}');
},
DeclarationTag(node, context) {
context.write('{');
// This is duplicated from esrap's handling of VariableDeclaration,
// which we need to do in order to omit the trailing semicolon that esrap would add.
const open = context.new();
const join = context.new();
const child_context = context.new();
context.append(child_context);
child_context.write(`${node.declaration.kind} `);
child_context.append(open);
const declarations = node.declaration.declarations;
let first = true;
for (const d of declarations) {
if (!first) child_context.append(join);
first = false;
child_context.visit(d);
}
const length = child_context.measure() + 2 * (declarations.length - 1);
const multiline = child_context.multiline || (declarations.length > 1 && length > 50);
if (multiline) {
context.multiline = true;
if (declarations.length > 1) open.indent();
join.write(',');
join.newline();
if (declarations.length > 1) context.dedent();
} else {
join.write(', ');
}
context.write('}');
},
DebugTag(node, context) {
context.write('{@debug ');
let started = false;

@ -161,6 +161,18 @@ export namespace AST {
};
}
/** A `{let ...}` or `{const ...}` tag */
export interface DeclarationTag extends BaseNode {
type: 'DeclarationTag';
declaration: VariableDeclaration;
/** @internal */
metadata: {
expression: ExpressionMetadata;
/** If this declaration tag contains an await expression, or needs to wait on other async, this is set */
promises_id?: Identifier;
};
}
/** A `{@debug ...}` tag */
export interface DebugTag extends BaseNode {
type: 'DebugTag';
@ -623,6 +635,7 @@ export namespace AST {
export type Tag =
| AST.AttachTag
| AST.ConstTag
| AST.DeclarationTag
| AST.DebugTag
| AST.ExpressionTag
| AST.HtmlTag

@ -686,7 +686,8 @@ export {
if_builder as if,
this_instance as this,
null_instance as null,
debugger_builder as debugger
debugger_builder as debugger,
new_builder as new
};
/**

@ -417,7 +417,7 @@ export class Boundary {
if (this.#pending_effect) current_batch.skip_effect(this.#pending_effect);
if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
current_batch.on_fork_commit(() => {
current_batch.oncommit(() => {
this.#handle_error(error);
});
} else {

@ -242,7 +242,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
var each_array = derived_safe_equal(() => {
var collection = get_collection();
return is_array(collection) ? collection : collection == null ? [] : array_from(collection);
return /** @type {V[]} */ (
is_array(collection) ? collection : collection == null ? [] : array_from(collection)
);
});
if (DEV) {

@ -343,6 +343,21 @@ function set_attributes(
var setters = get_setters(element);
if (
// the following login only make sense in a dev environment
current_renderer == null &&
node_name(element) === INPUT_TAG &&
'type' in next &&
('value' in next || '__value' in next)
) {
var type = next.type;
if (type !== current.type || (type === undefined && element.hasAttribute('type'))) {
current.type = type;
set_attribute(element, 'type', type, skip_warning);
}
}
// since key is captured we use const
for (const key in next) {
// let instead of var because referenced in a closure

@ -7,7 +7,6 @@ import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
import { tick, untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.js';
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
import { async_mode_flag } from '../../../../flags/index.js';

@ -1,4 +1,4 @@
/** @import { Blocker, Effect, Value } from '#client' */
/** @import { Blocker, Effect, Source, Value } from '#client' */
import { DESTROYED, STALE_REACTION } from '#client/constants';
import { DEV } from 'esm-env';
import {
@ -39,8 +39,22 @@ export function flatten(blockers, sync, async, fn) {
// Filter out already-settled blockers - no need to wait for them
var pending = blockers.filter((b) => !b.settled);
var deriveds = sync.map(d);
if (DEV) {
deriveds.forEach((d, i) => {
// TODO this is kinda useful for debugging but a lousy implementation —
// maybe the compiler could pass through the template string
d.label = sync[i]
.toString()
.replace('() => ', '')
.replaceAll('$.eager(() => ', '$state.eager(')
.replace(/\$\.get\((.+?)\)/g, (_, id) => id);
});
}
if (async.length === 0 && pending.length === 0) {
fn(sync.map(d));
fn(deriveds);
return;
}
@ -54,8 +68,10 @@ export function flatten(blockers, sync, async, fn) {
? Promise.all(pending.map((b) => b.promise))
: null;
/** @param {Value[]} values */
function finish(values) {
/**
* @param {Source[]} async
*/
function finish(async) {
if ((parent.f & DESTROYED) !== 0) {
return;
}
@ -63,7 +79,7 @@ export function flatten(blockers, sync, async, fn) {
restore();
try {
fn(values);
fn([...deriveds, ...async]);
} catch (error) {
invoke_error_boundary(error, parent);
}
@ -75,17 +91,14 @@ export function flatten(blockers, sync, async, fn) {
// Fast path: blockers but no async expressions
if (async.length === 0) {
/** @type {Promise<any>} */ (blocker_promise)
.then(() => finish(sync.map(d)))
.finally(decrement_pending);
/** @type {Promise<any>} */ (blocker_promise).then(() => finish([])).finally(decrement_pending);
return;
}
// Full path: has async expressions
function run() {
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => finish([...sync.map(d), ...result]))
.then(finish)
.catch((error) => invoke_error_boundary(error, parent))
.finally(decrement_pending);
}

@ -140,12 +140,6 @@ export class Batch {
*/
#discard_callbacks = new Set();
/**
* Callbacks that should run only when a fork is committed.
* @type {Set<(batch: Batch) => void>}
*/
#fork_commit_callbacks = new Set();
/**
* The number of async effects that are currently in flight
*/
@ -634,7 +628,6 @@ export class Batch {
discard() {
for (const fn of this.#discard_callbacks) fn(this);
this.#discard_callbacks.clear();
this.#fork_commit_callbacks.clear();
this.#unlink();
this.#deferred?.resolve();
@ -840,16 +833,6 @@ export class Batch {
this.#discard_callbacks.add(fn);
}
/** @param {(batch: Batch) => void} fn */
on_fork_commit(fn) {
this.#fork_commit_callbacks.add(fn);
}
run_fork_commit_callbacks() {
for (const fn of this.#fork_commit_callbacks) fn(this);
this.#fork_commit_callbacks.clear();
}
settled() {
return (this.#deferred ??= deferred()).promise;
}
@ -1410,10 +1393,6 @@ export function fork(fn) {
source.wv = increment_write_version();
}
batch.activate();
batch.run_fork_commit_callbacks();
batch.deactivate();
// trigger any `$state.eager(...)` expressions with the new state.
// eager effects don't get scheduled like other effects, so we
// can't just encounter them during traversal, we need to

@ -389,7 +389,9 @@ export function render_effect(fn, flags = 0) {
*/
export function template_effect(fn, sync = [], async = [], blockers = []) {
flatten(blockers, sync, async, (values) => {
create_effect(RENDER_EFFECT, () => fn(...values.map(get)));
create_effect(RENDER_EFFECT, () => {
fn(...values.map(get));
});
});
}
@ -522,7 +524,7 @@ export function destroy_effect(effect, remove_dom = true) {
removed = true;
}
set_signal_status(effect, DESTROYING);
effect.f |= DESTROYING;
destroy_effect_children(effect, remove_dom && !removed);
remove_reactions(effect, 0);

@ -49,11 +49,11 @@ export function update_pre_prop(fn, d = 1) {
/**
* The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`).
* Is passed the full `$$props` object and excludes the named props.
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, name?: string }>}}
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Set<string | symbol>, name?: string }>}}
*/
const rest_props_handler = {
get(target, key) {
if (target.exclude.includes(key)) return;
if (target.exclude.has(key)) return;
return target.props[key];
},
set(target, key) {
@ -65,7 +65,7 @@ const rest_props_handler = {
return false;
},
getOwnPropertyDescriptor(target, key) {
if (target.exclude.includes(key)) return;
if (target.exclude.has(key)) return;
if (key in target.props) {
return {
enumerable: true,
@ -75,17 +75,17 @@ const rest_props_handler = {
}
},
has(target, key) {
if (target.exclude.includes(key)) return false;
if (target.exclude.has(key)) return false;
return key in target.props;
},
ownKeys(target) {
return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key));
return Reflect.ownKeys(target.props).filter((key) => !target.exclude.has(key));
}
};
/**
* @param {Record<string, unknown>} props
* @param {string[]} exclude
* @param {Set<string>} exclude
* @param {string} [name]
* @returns {Record<string, unknown>}
*/

@ -132,11 +132,12 @@ export class SvelteURLSearchParams extends URLSearchParams {
* @returns {void}
*/
set(name, value) {
var previous = super.getAll(name).join('');
var previous = super.getAll(name);
super.set(name, value);
// can't use has(name, value), because for something like https://svelte.dev?foo=1&bar=2&foo=3
// if you set `foo` to 1, then foo=3 gets deleted whilst `has("foo", "1")` returns true
if (previous !== super.getAll(name).join('')) {
var current = super.getAll(name);
if (previous.length !== current.length || previous.some((value, i) => value !== current[i])) {
this.#update_url();
increment(this.#version);
}

@ -55,6 +55,44 @@ test('URLSearchParams.set', () => {
cleanup();
});
test('URLSearchParams.set updates when duplicate values collapse to the same joined string', () => {
const params = new SvelteURLSearchParams('a=ab&a=c');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.set('a', 'abc');
});
assert.deepEqual(log, ['a=ab&a=c', 'a=abc']);
cleanup();
});
test('URLSearchParams.set updates when duplicate values collapse to the same comma-joined string', () => {
const params = new SvelteURLSearchParams('a=a&a=b');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.set('a', 'a,b');
});
assert.deepEqual(log, ['a=a&a=b', 'a=a%2Cb']);
cleanup();
});
test('URLSearchParams.append', () => {
const params = new SvelteURLSearchParams();
const log: any = [];

@ -115,6 +115,25 @@ test('url.searchParams', () => {
cleanup();
});
test('url.searchParams.set updates url when duplicate values collapse to the same joined string', () => {
const url = new SvelteURL('https://svelte.dev?a=ab&a=c');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(url.href);
});
});
flushSync(() => {
url.searchParams.set('a', 'abc');
});
assert.deepEqual(log, ['https://svelte.dev/?a=ab&a=c', 'https://svelte.dev/?a=abc']);
cleanup();
});
test('url.search normalizes value', () => {
const url = new SvelteURL('https://svelte.dev');
const log: any = [];

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.55.10';
export const VERSION = '5.56.0';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,11 @@
import { test } from '../../test';
// A tag whose expression ends with a bare `/` at the end of the input used to
// make `find_matching_bracket` loop forever; it should error instead of hanging.
export default test({
error: {
code: 'unexpected_eof',
message: 'Unexpected end of input',
position: [12, 12]
}
});

@ -0,0 +1,5 @@
{#if true}
{let }
{const x = }
{let x = a / }
{/if}

@ -0,0 +1,145 @@
{
"css": null,
"js": [],
"start": 0,
"end": 54,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "IfBlock",
"elseif": false,
"start": 0,
"end": 54,
"test": {
"type": "Literal",
"start": 5,
"end": 9,
"loc": {
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 9
}
},
"value": true,
"raw": "true"
},
"consequent": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 10,
"end": 12,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "DeclarationTag",
"start": 12,
"end": 18,
"declaration": {
"type": "VariableDeclaration",
"kind": "let",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "",
"start": 17,
"end": 17
},
"init": null,
"start": 17,
"end": 17
}
],
"start": 13,
"end": 17
}
},
{
"type": "Text",
"start": 18,
"end": 20,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "DeclarationTag",
"start": 20,
"end": 32,
"declaration": {
"type": "VariableDeclaration",
"kind": "const",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "",
"start": 31,
"end": 31
},
"init": null,
"start": 31,
"end": 31
}
],
"start": 21,
"end": 31
}
},
{
"type": "Text",
"start": 32,
"end": 34,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "DeclarationTag",
"start": 34,
"end": 48,
"declaration": {
"type": "VariableDeclaration",
"kind": "let",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "",
"start": 47,
"end": 47
},
"init": null,
"start": 47,
"end": 47
}
],
"start": 35,
"end": 47
}
},
{
"type": "Text",
"start": 48,
"end": 49,
"raw": "\n",
"data": "\n"
}
]
},
"alternate": null
}
]
},
"options": null
}

@ -0,0 +1,92 @@
{
"css": null,
"js": [],
"start": 0,
"end": 36,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "ExpressionTag",
"start": 0,
"end": 36,
"expression": {
"type": "BinaryExpression",
"start": 1,
"end": 35,
"loc": {
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 1,
"column": 35
}
},
"left": {
"type": "Identifier",
"start": 1,
"end": 5,
"loc": {
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 1,
"column": 5
}
},
"name": "type"
},
"operator": "instanceof",
"right": {
"type": "Identifier",
"start": 29,
"end": 35,
"loc": {
"start": {
"line": 1,
"column": 29
},
"end": {
"line": 1,
"column": 35
}
},
"name": "Object",
"leadingComments": [
{
"type": "Block",
"value": " probe ",
"start": 17,
"end": 28
}
]
}
}
}
]
},
"options": null,
"comments": [
{
"type": "Block",
"value": " probe ",
"start": 17,
"end": 28,
"loc": {
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 28
}
}
}
]
}

@ -0,0 +1,7 @@
{#if visible}
{let count = 1}
{const doubled = count * 2}
{const label = 'count'}
{const format = (value) => `${label}: ${value}`}
<p>{format(doubled)}</p>
{/if}

@ -0,0 +1,7 @@
{#if visible}
{let count = 1}
{const doubled = count * 2}
{const label = 'count'}
{const format = (value) => `${label}: ${value}`}
<p>{format(doubled)}</p>
{/if}

@ -0,0 +1,13 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['async-server', 'client', 'hydrate'],
ssrHtml: `<h1>Hello, world!</h1> 5 01234 5 sync 6 5 0 10`,
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, `<h1>Hello, world!</h1> 5 01234 5 sync 6 5 0 10`);
}
});

@ -0,0 +1,31 @@
<script>
let name = $state('world');
</script>
<svelte:boundary>
{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}!`)}
<h1>{greeting}</h1>
{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}
</svelte:boundary>

@ -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,
`
<button>name</button>
<button>change name</button>
<p>Hello name</p>
<div><span>nested Hi name</span></div>
`
);
top.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>other</button>
<button>change name</button>
<p>Hello name</p>
<div><span>nested Hi name</span></div>
`
);
change.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>other</button>
<button>change name</button>
<p>Hello other</p>
<div><span>nested Hi other</span></div>
`
);
}
});

@ -0,0 +1,20 @@
<script>
const id = 'name';
const top_id = await 'name';
</script>
{let name = $state(top_id)}
<button onclick={() => name = 'other'}>{name}</button>
{#if id}
{let name = $state(await id)}
{let greeting = $derived(await `Hello ${name}`)}
<button onclick={() => name = 'other'}>change name</button>
<p>{greeting}</p>
<div>
{const nested = 'nested'}
{const greeting2 = $derived(await `Hi ${name}`)}
<span>{nested} {greeting2}</span>
</div>
{/if}

@ -0,0 +1,17 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>increment</button><p>count: 0</p><p>doubled: 0</p><p>quadrupled: 0</p>`,
async test({ assert, target }) {
const [button] = target.querySelectorAll('button');
button.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button><p>count: 1</p><p>doubled: 2</p><p>quadrupled: 4</p>`
);
}
});

@ -0,0 +1,10 @@
<!-- a later declarator can reference an earlier one within the same tag... -->
{let count = $state(0), doubled = $derived(count * 2)}
<!-- ...and leading whitespace is tolerated -->
{ let quadrupled = $derived(doubled * 2) }
<button onclick={() => (count += 1)}>increment</button>
<p>count: {count}</p>
<p>doubled: {doubled}</p>
<p>quadrupled: {quadrupled}</p>

@ -0,0 +1,13 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: '<button>0 | 0</button>',
async test({ assert, target }) {
const [increment] = target.querySelectorAll('button');
increment.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>1 | 2</button>');
}
});

@ -0,0 +1,3 @@
{let count = $state(0)}
{let doubled = $derived(count * 2)}
<button onclick={() => (count += 1)}>{count} | {doubled}</button>

@ -0,0 +1,37 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>top 2</button><button>toggle</button><button>2</button><p>4 total</p><div><span>nested</span></div><div><span>nested</span></div>`,
async test({ assert, target }) {
const [top, toggle, increment] = target.querySelectorAll('button');
top.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>top 4</button><button>toggle</button><button>2</button><p>4 total</p><div><span>nested</span></div><div><span>nested</span></div>`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>top 4</button><button>toggle</button><button>3</button><p>6 total</p><div><span>nested</span></div><div><span>nested</span></div>`
);
toggle.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>top 4</button><button>toggle</button><div><span>nested</span></div>`
);
toggle.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>top 4</button><button>toggle</button><button>2</button><p>4 total</p><div><span>nested</span></div><div><span>nested</span></div>`
);
}
});

@ -0,0 +1,29 @@
<script>
let visible = $state(true);
let initial = $state(2);
</script>
{let top = $state(1)}
{let top_doubled = $derived(top * 2)}
<button onclick={() => (top += 1)}>top {top_doubled}</button>
<button onclick={() => (visible = !visible)}>toggle</button>
{#if visible}
{let counter = $state({ value: initial })}
{let doubled = $derived(counter.value * 2)}
{const suffix = ' total'}
{const format = (value) => `${value}${suffix}`}
<button onclick={() => (counter.value += 1)}>{counter.value}</button>
<p>{format(doubled)}</p>
<div>
{const doubled = 'nested'}
<span>{doubled}</span>
</div>
{/if}
<div>
{const nested = 'nested'}
<span>{nested}</span>
</div>

@ -0,0 +1,15 @@
import { test } from '../../test';
const value = 'line1\nline2\nline3';
export default test({
mode: ['client'],
test({ assert, target }) {
const [spread_first, type_first] = target.querySelectorAll('input');
assert.equal(spread_first?.type, 'hidden');
assert.equal(spread_first?.value, value);
assert.equal(type_first?.type, 'hidden');
assert.equal(type_first?.value, value);
}
});

@ -0,0 +1,7 @@
<script>
const value = 'line1\nline2\nline3';
const spread = { name: 'field', value };
</script>
<input {...spread} type="hidden" />
<input type="hidden" {...spread} />

@ -3,7 +3,7 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/client';
var root_1 = $.from_html(`<p> </p>`);
var root = $.from_html(`<p> </p>`);
export default function Async_const($$anchor) {
var fragment = $.comment();
@ -19,7 +19,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);

@ -0,0 +1,44 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<p class="x">hello</p>`);
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);
}

@ -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('<!--[0-->');
$$renderer.push(`<p class="x">hello</p>`);
} else {
$$renderer.push('<!--[-1-->');
$$renderer.push(`<p class="x">hello</p>`);
}
$$renderer.push(`<!--]--> `);
if (b) {
$$renderer.push('<!--[0-->');
$$renderer.push(`<p class="x">hello</p>`);
} else {
$$renderer.push('<!--[-1-->');
}
$$renderer.push(`<!--]-->`);
}

@ -0,0 +1,14 @@
<script>
let { a, b } = $props();
</script>
<!-- identical static markup repeated across branches should share one hoisted template -->
{#if a}
<p class="x">hello</p>
{:else}
<p class="x">hello</p>
{/if}
{#if b}
<p class="x">hello</p>
{/if}

@ -3,14 +3,14 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root_1 = $.from_html(`<button type="button">B</button>`);
var root = $.from_html(`<button type="button">B</button>`);
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);

@ -3,14 +3,14 @@ import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root_1 = $.from_html(`<p></p>`);
var root = $.from_html(`<p></p>`);
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);

@ -2,10 +2,12 @@ import 'svelte/internal/init-operations';
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];

@ -5,63 +5,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(`<option>Snippet</option>`);
var root_2 = $.from_html(`<option>Rendered</option>`);
var root_3 = $.from_html(`<option>Rendered in group</option>`);
var root_4 = $.from_html(`<option>Conditional</option>`);
var root = $.from_html(`<option>Snippet</option>`);
var root_1 = $.from_html(`<option>Rendered</option>`);
var root_2 = $.from_html(`<option>Rendered in group</option>`);
var root_3 = $.from_html(`<option>Conditional</option>`);
var option_content = $.from_html(`<span>Rich</span>`, 1);
var root_5 = $.from_html(`<option> </option>`);
var root_6 = $.from_html(`<option>Visible</option>`);
var root_7 = $.from_html(`<option>Keyed</option>`);
var root_4 = $.from_html(`<option> </option>`);
var root_5 = $.from_html(`<option>Visible</option>`);
var root_6 = $.from_html(`<option>Keyed</option>`);
var select_content = $.from_html(`<!>`, 1);
var root_8 = $.from_html(`<option> </option>`);
var option_content_1 = $.from_html(`<strong>Bold</strong>`, 1);
var root_9 = $.from_html(`<option> </option>`);
var option_content_2 = $.from_html(`<em>Italic</em> text`, 1);
var option_content_3 = $.from_html(`<span> </span>`, 1);
var root_10 = $.from_html(`<option><!></option>`);
var root_12 = $.from_html(`<option> </option>`);
var root_13 = $.from_html(`<option>Boundary</option>`);
var root_7 = $.from_html(`<option><!></option>`);
var root_8 = $.from_html(`<option>Boundary</option>`);
var option_content_4 = $.from_html(`<span>Rich in boundary</span>`, 1);
var root_14 = $.from_html(`<option><!></option>`);
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(`<select><option><!></option></select> <select></select> <select><!></select> <select><!></select> <select><!></select> <select></select> <select><optgroup label="Group"><option><!></option></optgroup></select> <select><optgroup label="Group"></optgroup></select> <select><option><!></option></select> <select></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><optgroup label="Group"><!></optgroup></select> <select><optgroup label="Group"><!></optgroup></select> <select><option><!></option></select> <select><!></select> <select><!></select>`, 1);
var root_9 = $.from_html(`<select><option><!></option></select> <select></select> <select><!></select> <select><!></select> <select><!></select> <select></select> <select><optgroup label="Group"><option><!></option></optgroup></select> <select><optgroup label="Group"></optgroup></select> <select><option><!></option></select> <select></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><!></select> <select><optgroup label="Group"><!></optgroup></select> <select><optgroup label="Group"><!></optgroup></select> <select><option><!></option></select> <select><!></select> <select><!></select>`, 1);
export default function Select_with_rich_content($$anchor) {
let items = [1, 2, 3];
let show = true;
let html = '<option>From HTML</option>';
var fragment = root();
var fragment = root_9();
var select = $.first_child(fragment);
var option_4 = $.child(select);
@ -77,7 +65,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);
@ -102,7 +90,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);
};
@ -118,7 +106,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);
});
@ -140,7 +128,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);
@ -178,7 +166,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);
@ -216,7 +204,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);
@ -243,7 +231,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);
@ -275,7 +263,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);
});
@ -286,7 +274,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);
@ -304,7 +292,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, {});
@ -315,7 +303,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);
@ -326,7 +314,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);
@ -338,7 +326,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, {});
@ -352,7 +340,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);
@ -366,7 +354,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, () => '<strong>Bold HTML</strong>');
@ -379,7 +367,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) => {
@ -393,7 +381,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);
{

@ -0,0 +1,14 @@
[
{
"code": "declaration_tag_invalid_type",
"message": "Declaration tags must be `let` or `const` declarations",
"start": {
"line": 28,
"column": 2
},
"end": {
"line": 28,
"column": 20
}
}
]

@ -0,0 +1,29 @@
<script lang="ts"></script>
{#if true}
<!-- check that these doesn't trigger already -->
{type}
{type }
{type && foo}
{type || bar}
{type % 2}
{type .x}
{type ?.x}
{type ()}
{type [1]}
{type `tag`}
{type === 'all' ? 'All types' : type}
{type !== 'all'}
{type == 'all'}
{type != 'all'}
{type + 1}
{type - 1}
{type * 2}
{type / 2}
{type > 1}
{type instanceof Foo}
{type instanceof /* comment */ Object}
{type in foo}
<!-- ... this one should trigger though -->
{type foo = boolean}
{/if}

@ -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
}
}
]

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save