Merge branch 'main' into issue-18419-SvelteDate

pull/18509/head
JY Wey 3 weeks ago committed by GitHub
commit 4c22d7b296
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: block template store subscriptions on the promise that assigns the store

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: route $derived teardown errors through invoke_error_boundary

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: cancel deferred event listeners during cleanup

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve global CSS in components without scopable elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reduce SSR render result garbage collection

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: warn on undeclared shorthand event handlers on `<svelte:window>`, `<svelte:document>` and `<svelte:body>`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: resolve the fallback of an each block in the enclosing scope

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: speed up parser interactions with Acorn or avoid them where possible

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: prevent onoutroend from firing twice when compilerOptions.hmr is true

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: preserve whitespace after inline elements when printing

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent effect tree of batches from interfering with each other

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: run `onDestroy` callbacks when a server render throws

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: serialize input default values during server rendering

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove `WAS_MARKED` flag in favor of `Set`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: never apply class hash to elements inside `<svelte:head>`

@ -1,5 +0,0 @@
---
'svelte': patch
---
perf: O(n²)→O(n) Map lookups for legacy `$:` reactive statement ordering

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: distinct memoizer on style/class directives

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't turn component instances stored in `$state` into state proxies

@ -1,5 +0,0 @@
---
'svelte': minor
---
feat: export `RenderOutput`, `SyncRenderOutput`, `Csp` and `Sha256Source` from `svelte/server`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: throw `set_context_after_init` when `setContext` is called after an `await` during SSR

@ -1,5 +0,0 @@
---
"svelte": patch
---
perf: optimize simple object destructuring in `@const` tags

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't resurrect outroing elements when an ancestor block is paused and resumed

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep `$state.eager` when used as a variable initializer

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: avoid regex matching in parser where possible

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: prevent `selectedcontent` mutation from changing the selected option

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: avoid `NaN` keyframe values in `slide` transition for elements without a layout box

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: in non-async mode, only push variable to current_sources when active_reaction is updating

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: recognise `aria-braillelabel` and `aria-brailleroledescription` as known ARIA attributes

@ -1,5 +0,0 @@
---
'svelte': patch
---
chore: clarify when `$effect.pre` runs relative to DOM updates

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: scope SSR boundary failed snippets to their boundary

@ -0,0 +1,346 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.resolve(fileURLToPath(import.meta.url), '../..');
const compiler_path = 'packages/svelte/src/compiler/';
/**
* Merge V8 profiles from a Vitest run into a compiler-only flame graph and hotspot summary.
* Samples retain callees outside the compiler once execution has entered compiler code.
* @param {string} run_dir
*/
export function analyze_compiler_profiles(run_dir) {
const raw_dir = path.join(run_dir, 'raw');
const profile_files = fs
.readdirSync(raw_dir)
.filter((file) => file.endsWith('.cpuprofile'))
.sort();
if (profile_files.length === 0) {
throw new Error(`No CPU profiles found in ${raw_dir}`);
}
/** @type {Array<{ name: string, file?: string, line?: number, col?: number }>} */
const frames = [];
/** @type {Map<string, number>} */
const frame_indices = new Map();
/** @type {Map<string, { stack: number[], weight: number }>} */
const stacks = new Map();
/** @type {Map<string, Hotspot>} */
const hotspots = new Map();
/** @type {Map<string, Hotspot>} */
const files = new Map();
let total_time = 0;
let compiler_time = 0;
let garbage_collection_time = 0;
let total_samples = 0;
let compiler_samples = 0;
for (const profile_file of profile_files) {
let profile;
try {
profile = JSON.parse(fs.readFileSync(path.join(raw_dir, profile_file), 'utf8'));
} catch (error) {
throw new Error(`Could not parse CPU profile ${profile_file}`, { cause: error });
}
const nodes = Array.isArray(profile.nodes) ? profile.nodes : [];
const samples = Array.isArray(profile.samples) ? profile.samples : [];
const time_deltas = Array.isArray(profile.timeDeltas) ? profile.timeDeltas : [];
const nodes_by_id = new Map(nodes.map((node) => [node.id, node]));
const parents = new Map();
for (const node of nodes) {
for (const child of node.children || []) {
parents.set(child, node.id);
}
}
for (let i = 0; i < samples.length; i += 1) {
const weight = typeof time_deltas[i] === 'number' ? time_deltas[i] : 1;
const stack = get_stack(samples[i], nodes_by_id, parents);
total_time += weight;
total_samples += 1;
if (stack.at(-1)?.function_name === '(garbage collector)') {
garbage_collection_time += weight;
}
const compiler_index = stack.findIndex((frame) => frame.url.includes(compiler_path));
if (compiler_index === -1) continue;
const compiler_stack = stack.slice(compiler_index);
const stack_indices = compiler_stack.map(get_frame_index);
const stack_key = stack_indices.join(',');
const existing_stack = stacks.get(stack_key);
if (existing_stack) {
existing_stack.weight += weight;
} else {
stacks.set(stack_key, { stack: stack_indices, weight });
}
compiler_time += weight;
compiler_samples += 1;
const leaf = compiler_stack.at(-1);
if (leaf) {
get_hotspot(hotspots, frame_key(leaf), leaf).self += weight;
get_hotspot(files, leaf.url || '(native)', {
...leaf,
function_name: leaf.url || '(native)'
}).self += weight;
}
const seen_frames = new Set();
const seen_files = new Set();
for (const frame of compiler_stack) {
const key = frame_key(frame);
if (!seen_frames.has(key)) {
get_hotspot(hotspots, key, frame).inclusive += weight;
seen_frames.add(key);
}
const file = frame.url || '(native)';
if (!seen_files.has(file)) {
get_hotspot(files, file, { ...frame, function_name: file }).inclusive += weight;
seen_files.add(file);
}
}
}
}
if (compiler_time === 0) {
throw new Error('The CPU profiles contain no samples from packages/svelte/src/compiler');
}
const hotspot_rows = rank(hotspots, compiler_time);
const file_rows = rank(files, compiler_time);
const compact_stacks = [...stacks.values()];
const summary = {
profile_files: profile_files.length,
total_samples,
compiler_samples,
total_time_microseconds: total_time,
compiler_time_microseconds: compiler_time,
garbage_collection_time_microseconds: garbage_collection_time,
compiler_share_percent: (compiler_time * 100) / total_time,
hotspots: hotspot_rows,
files: file_rows
};
const speedscope = {
$schema: 'https://www.speedscope.app/file-format-schema.json',
name: `Svelte compiler: ${path.basename(run_dir)}`,
exporter: 'Svelte compiler test profiler',
activeProfileIndex: 0,
shared: { frames },
profiles: [
{
type: 'sampled',
name: 'Compiler-active samples from pnpm test',
unit: 'microseconds',
startValue: 0,
endValue: compiler_time,
samples: compact_stacks.map((entry) => entry.stack),
weights: compact_stacks.map((entry) => entry.weight)
}
]
};
fs.writeFileSync(
path.join(run_dir, 'flamegraph.speedscope.json'),
`${JSON.stringify(speedscope)}\n`
);
fs.writeFileSync(path.join(run_dir, 'summary.json'), `${JSON.stringify(summary, null, '\t')}\n`);
fs.writeFileSync(path.join(run_dir, 'summary.md'), render_markdown(summary));
return summary;
/** @param {Frame} frame */
function get_frame_index(frame) {
const key = frame_key(frame);
const existing = frame_indices.get(key);
if (existing !== undefined) return existing;
const index = frames.length;
const location = frame.url ? ` (${frame.url}:${frame.line})` : '';
frames.push({
name: `${frame.function_name}${location}`,
...(frame.url ? { file: frame.url, line: frame.line, col: frame.column } : {})
});
frame_indices.set(key, index);
return index;
}
}
/**
* @param {number} leaf_id
* @param {Map<number, any>} nodes_by_id
* @param {Map<number, number>} parents
* @returns {Frame[]}
*/
function get_stack(leaf_id, nodes_by_id, parents) {
/** @type {Frame[]} */
const stack = [];
const seen = new Set();
let id = leaf_id;
while (typeof id === 'number' && !seen.has(id)) {
seen.add(id);
const node = nodes_by_id.get(id);
if (!node) break;
const call_frame = node.callFrame || {};
stack.push({
function_name: call_frame.functionName || '(anonymous)',
url: normalize_url(call_frame.url || ''),
line: typeof call_frame.lineNumber === 'number' ? call_frame.lineNumber + 1 : 0,
column: typeof call_frame.columnNumber === 'number' ? call_frame.columnNumber + 1 : 0
});
id = parents.get(id);
}
return stack.reverse();
}
/** @param {string} url */
function normalize_url(url) {
if (!url) return '';
let pathname = url.replace(/^\/\@fs\//, '/').replace(/[?#].*$/, '');
if (pathname.startsWith('file://')) {
try {
pathname = fileURLToPath(pathname);
} catch {
return url;
}
}
if (path.isAbsolute(pathname)) {
const relative = path.relative(root, pathname);
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
pathname = relative;
}
}
return pathname.replaceAll(path.sep, '/');
}
/** @param {Frame} frame */
function frame_key(frame) {
return `${frame.function_name}\0${frame.url}\0${frame.line}\0${frame.column}`;
}
/**
* @param {Map<string, Hotspot>} collection
* @param {string} key
* @param {Frame} frame
*/
function get_hotspot(collection, key, frame) {
let hotspot = collection.get(key);
if (!hotspot) {
hotspot = { ...frame, self: 0, inclusive: 0 };
collection.set(key, hotspot);
}
return hotspot;
}
/**
* @param {Map<string, Hotspot>} collection
* @param {number} total
*/
function rank(collection, total) {
return [...collection.values()]
.map((hotspot) => ({
function: hotspot.function_name,
url: hotspot.url,
line: hotspot.line,
column: hotspot.column,
self_microseconds: hotspot.self,
self_percent: (hotspot.self * 100) / total,
inclusive_microseconds: hotspot.inclusive,
inclusive_percent: (hotspot.inclusive * 100) / total
}))
.sort(
(a, b) =>
b.self_microseconds - a.self_microseconds ||
b.inclusive_microseconds - a.inclusive_microseconds
);
}
/** @param {ReturnType<typeof analyze_compiler_profiles>} summary */
function render_markdown(summary) {
const lines = [
'# Svelte compiler CPU profile',
'',
'Open `flamegraph.speedscope.json` in [Speedscope](https://www.speedscope.app/) for the interactive flame graph.',
'',
'## Coverage',
'',
'| Metric | Value |',
'| --- | ---: |',
`| Raw profiles | ${summary.profile_files} |`,
`| All profiled CPU time | ${format_time(summary.total_time_microseconds)} |`,
`| Compiler-active CPU time | ${format_time(summary.compiler_time_microseconds)} |`,
`| Compiler share | ${summary.compiler_share_percent.toFixed(2)}% |`,
`| All-process garbage collection | ${format_time(summary.garbage_collection_time_microseconds)} |`,
'',
'Compiler-active time includes external callees while a compiler frame is on the stack. Garbage collection is reported for context but cannot be attributed to compiler stacks by V8.',
'',
'## Top Self Hotspots',
'',
'| Rank | Function | Location | Self | Self % | Inclusive | Inclusive % |',
'| ---: | --- | --- | ---: | ---: | ---: | ---: |'
];
for (const [index, row] of summary.hotspots.slice(0, 50).entries()) {
lines.push(render_row(index, row));
}
lines.push(
'',
'## Top Files',
'',
'| Rank | File | Self | Self % | Inclusive | Inclusive % |',
'| ---: | --- | ---: | ---: | ---: | ---: |'
);
for (const [index, row] of summary.files.slice(0, 50).entries()) {
lines.push(
`| ${index + 1} | ${escape_cell(row.function)} | ${format_time(row.self_microseconds)} | ${row.self_percent.toFixed(2)}% | ${format_time(row.inclusive_microseconds)} | ${row.inclusive_percent.toFixed(2)}% |`
);
}
return `${lines.join('\n')}\n`;
}
/** @param {number} index @param {ReturnType<typeof rank>[number]} row */
function render_row(index, row) {
const location = row.url ? `${row.url}:${row.line}:${row.column}` : '(native)';
return `| ${index + 1} | ${escape_cell(row.function)} | ${escape_cell(location)} | ${format_time(row.self_microseconds)} | ${row.self_percent.toFixed(2)}% | ${format_time(row.inclusive_microseconds)} | ${row.inclusive_percent.toFixed(2)}% |`;
}
/** @param {number} microseconds */
function format_time(microseconds) {
return `${(microseconds / 1000).toFixed(1)} ms`;
}
/** @param {string} value */
function escape_cell(value) {
return value.replaceAll('|', '\\|');
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const run_dir = process.argv[2];
if (!run_dir) {
console.error('Usage: node benchmarking/analyze-compiler-profile.js <profile-run-directory>');
process.exit(1);
}
analyze_compiler_profiles(path.resolve(run_dir));
}
/**
* @typedef {{ function_name: string, url: string, line: number, column: number }} Frame
* @typedef {Frame & { self: number, inclusive: number }} Hotspot
*/

@ -0,0 +1,3 @@
import { parser_benchmarks } from './parser.bench.js';
export const compiler_benchmarks = parser_benchmarks;

@ -0,0 +1,58 @@
import { parse } from '../../../packages/svelte/src/compiler/phases/1-parse/index.js';
import { fastest_test } from '../../utils.js';
const elements_source = Array.from(
{ length: 1000 },
(_, i) =>
`<!-- item ${i} --><section data-index="${i}" class="card card-${i}" title="plain text ${i}"><h2>Item ${i} has a reasonably long entity-free text value</h2><input disabled value="prefix {value} suffix"><textarea>plain {value} text</textarea></section>`
).join('\n');
const script_style_source = `<script>
// line comment
/* block comment */
const value = 42;
</script>
<style>
${Array.from(
{ length: 1000 },
(_, i) => `.item-${i} { /* property ${i} */ color: red; margin-inline: ${i}px; }`
).join('\n')}
</style>`;
const typescript_source = `${Array.from(
{ length: 1000 },
(_, i) => `<div data-index="${i}">Item ${i}</div>`
).join('\n')}
{value as number}
<script lang="ts">
let value: number = 42;
</script>${' \n'.repeat(1000)}`;
/**
* @param {string} label
* @param {string} source
* @param {number} iterations
*/
function create_parser_benchmark(label, source, iterations) {
return {
label,
fn: async () => {
for (let i = 0; i < iterations; i++) {
parse(source);
}
return await fastest_test(10, () => {
for (let i = 0; i < iterations; i++) {
parse(source);
}
});
}
};
}
export const parser_benchmarks = [
create_parser_benchmark('parser_elements', elements_source, 5),
create_parser_benchmark('parser_script_style', script_style_source, 50),
create_parser_benchmark('parser_typescript', typescript_source, 25)
];

@ -13,7 +13,7 @@ import {
sbench_create_4to1,
sbench_create_signals
} from './sbench.js';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { create_test } from './util.js';
// This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark)
@ -39,7 +39,8 @@ for (const file of fs.readdirSync(`${dirname}/tests`)) {
const name = file.replace('.bench.js', '');
const module = await import(`${dirname}/tests/${file}`);
const module_url = pathToFileURL(path.join(dirname, 'tests', file));
const module = await import(module_url.href);
const { owned, unowned } = create_test(name, module.default);
reactivity_benchmarks.push(owned, unowned);

@ -25,12 +25,12 @@ export const wrapper_bench = {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
render(App);
render(App).body;
}
return await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
render(App);
render(App).body;
}
});
}

@ -0,0 +1,43 @@
# Compiler CPU profiling
Run the full test suite with V8 CPU profiling enabled in every Vitest worker:
```sh
pnpm profile:compiler
```
Arguments are passed to `pnpm test`, so a smaller smoke run can target a suite:
```sh
pnpm profile:compiler snapshot
```
Each run is stored under `benchmarking/.profiles/compiler-tests/<timestamp>`. Set
`SVELTE_PROFILE_NAME` to give a run a stable name; existing runs are never overwritten.
`benchmarking/.profiles/compiler-tests/latest.txt` contains the latest successful run name.
Each run contains:
- `raw/*.cpuprofile`: the original V8 profile from every Vitest worker
- `flamegraph.speedscope.json`: compiler-active stacks merged for use in
[Speedscope](https://www.speedscope.app/)
- `summary.md`: ranked self/inclusive hotspots and files
- `summary.json`: the same measurements for further analysis
- `manifest.json`: the command, revision, timestamps, and test result
The merged profile includes downstream callees while a compiler source frame is on the stack.
This captures parser, walker, and printer dependencies without including unrelated test runtime.
V8 reports garbage collection outside the originating stack, so the summary reports process-wide
GC separately rather than attributing it to the compiler.
The default sampling interval is 5 ms. It can be changed for shorter, focused runs:
```sh
SVELTE_CPU_PROF_INTERVAL=1000 pnpm profile:compiler snapshot
```
Existing raw profiles can be analyzed again without rerunning tests:
```sh
node benchmarking/analyze-compiler-profile.js benchmarking/.profiles/compiler-tests/<run>
```

@ -0,0 +1,94 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { analyze_compiler_profiles } from './analyze-compiler-profile.js';
const root = path.resolve(fileURLToPath(import.meta.url), '../..');
const profiles_root = path.join(root, 'benchmarking/.profiles/compiler-tests');
const requested_name = process.env.SVELTE_PROFILE_NAME;
const run_name = safe(requested_name || new Date().toISOString().replaceAll(':', '-'));
const run_dir = path.join(profiles_root, run_name);
const raw_dir = path.join(run_dir, 'raw');
const test_args = process.argv.slice(2);
const sampling_interval = process.env.SVELTE_CPU_PROF_INTERVAL || '5000';
if (fs.existsSync(run_dir)) {
console.error(`Profile run already exists: ${path.relative(root, run_dir)}`);
process.exit(1);
}
fs.mkdirSync(raw_dir, { recursive: true });
const revision = spawnSync('git', ['rev-parse', 'HEAD'], {
cwd: root,
encoding: 'utf8'
});
const started_at = new Date().toISOString();
const manifest_file = path.join(run_dir, 'manifest.json');
const command = [
'pnpm',
'test',
'--execArgv=--cpu-prof',
`--execArgv=--cpu-prof-dir=${raw_dir}`,
`--execArgv=--cpu-prof-interval=${sampling_interval}`,
...test_args
];
write_manifest({
command,
revision: revision.status === 0 ? revision.stdout.trim() : null,
started_at,
status: 'running'
});
console.log(`Compiler profiles will be written to ${path.relative(root, run_dir)}`);
const result = spawnSync(command[0], command.slice(1), {
cwd: root,
stdio: 'inherit'
});
let analysis_error = null;
try {
analyze_compiler_profiles(run_dir);
fs.writeFileSync(path.join(profiles_root, 'latest.txt'), `${run_name}\n`);
} catch (error) {
analysis_error = error instanceof Error ? error.stack || error.message : String(error);
console.error(analysis_error);
}
write_manifest({
command,
revision: revision.status === 0 ? revision.stdout.trim() : null,
started_at,
finished_at: new Date().toISOString(),
status: result.status === 0 && analysis_error === null ? 'completed' : 'failed',
test_exit_code: result.status,
test_signal: result.signal,
analysis_error
});
if (result.error) {
console.error(result.error);
}
if (result.status === 0 && analysis_error === null) {
console.log(
`Compiler flame graph: ${path.relative(root, path.join(run_dir, 'flamegraph.speedscope.json'))}`
);
console.log(`Compiler hotspot summary: ${path.relative(root, path.join(run_dir, 'summary.md'))}`);
} else {
process.exitCode = result.status || 1;
}
/** @param {string} value */
function safe(value) {
return value.replace(/[^a-z0-9._-]+/gi, '_');
}
/** @param {Record<string, unknown>} manifest */
function write_manifest(manifest) {
fs.writeFileSync(manifest_file, `${JSON.stringify(manifest, null, '\t')}\n`);
}

@ -1,6 +1,7 @@
import { fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import * as $ from '../packages/svelte/src/internal/client/index.js';
import { compiler_benchmarks } from './benchmarks/compiler/index.js';
import { reactivity_benchmarks } from './benchmarks/reactivity/index.js';
import { ssr_benchmarks } from './benchmarks/ssr/index.js';
import { with_cpu_profile } from './utils.js';
@ -11,7 +12,9 @@ const single = process.env.BENCH_SINGLE;
if (single) {
// child mode — run a single benchmark and report the result to the parent
const benchmark = [...reactivity_benchmarks, ...ssr_benchmarks].find((b) => b.label === single);
const benchmark = [...compiler_benchmarks, ...reactivity_benchmarks, ...ssr_benchmarks].find(
(b) => b.label === single
);
if (!benchmark) {
throw new Error(`Unknown benchmark ${single}`);
@ -33,6 +36,13 @@ if (single) {
const filters = process.argv.slice(2);
const suites = [
// Commenting out because we rarely need to run it
// {
// benchmarks: compiler_benchmarks.filter(
// (b) => filters.length === 0 || filters.some((f) => b.label.includes(f))
// ),
// name: 'compiler benchmarks'
// },
{
benchmarks: reactivity_benchmarks.filter(
(b) => filters.length === 0 || filters.some((f) => b.label.includes(f))

@ -251,6 +251,19 @@ You can give the `<select>` a default value by adding a `selected` attribute to
</select>
```
Since 5.57.0, if a `<select>` has a `defaultValue` and is part of a form, it will revert to that value instead of the empty string when the form is reset. Note that for the initial render the value of the binding takes precedence unless it is `null` or `undefined`.
```svelte
<form>
<select bind:value defaultValue="b">
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<input type="reset" value="Reset">
</form>
```
## `<audio>`
`<audio>` elements have their own set of bindings — five two-way ones...

@ -4,7 +4,7 @@ title: Context
Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling').
By creating a `[get, set]` pair of functions with `createContext`, you can set the context in a parent component and get it in a child component:
By creating a `[get, set, has]` triplet of functions with `createContext`, you can set the context in a parent component and get it in a child component:
<!-- codeblock:start {"title":"Context","selected":"context.ts"} -->
```svelte
@ -165,9 +165,9 @@ Svelte will warn you if you get it wrong.
Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions).
## Component testing
## Mounting components with context
When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing:
To mount a component with specific context, create a wrapper component that sets the context before rendering the component. This is useful for [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), or any other scenario that needs to provide context through `mount`. As of version 5.49, you can do this sort of thing:
```js
import { mount, unmount } from 'svelte';
@ -193,6 +193,8 @@ test('MyComponent', () => {
This approach also works with [`hydrate`](imperative-component-api#hydrate) and [`render`](imperative-component-api#render).
The context set by the wrapper only applies to that mounted component tree. Each call to `mount`, `hydrate` or `render` creates a separate wrapper instance, so the context does not leak into other mounted components.
## Replacing global state
When you have state shared by many different components, you might be tempted to put it in its own module and just import it wherever it's needed:

@ -225,14 +225,6 @@ Rest element properties of `$props()` such as `%property%` are readonly
The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
```
### set_context_after_init
```
`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
```
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### state_descriptors_fixed
```

@ -360,7 +360,7 @@ Enforce that heading elements (`h1`, `h2`, etc.) and anchors have content and th
'%event%' event must be accompanied by '%accompanied_by%' event
```
Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` and `onblur`, respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users.
Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` (or `onfocusin`) and `onblur` (or `onfocusout`), respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users.
```svelte
<!-- A11y: onmouseover must be accompanied by onfocus -->

@ -78,7 +78,15 @@ Certain lifecycle methods can only be used during component initialisation. To f
Context was not set in the current component or any of its ancestors
```
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
### set_context_after_init
```
`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
```
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### snippet_without_render_tag

@ -19,6 +19,7 @@
"lint": "eslint && prettier --check .",
"format": "prettier --write .",
"test": "vitest run",
"profile:compiler": "node ./benchmarking/profile-compiler.js",
"changeset:version": "changeset version && pnpm -r generate:version && git add --all",
"changeset:publish": "changeset publish",
"bench": "NODE_ENV=production node --allow-natives-syntax ./benchmarking/run.js",
@ -37,7 +38,7 @@
"eslint-plugin-lube": "^0.5.1",
"eslint-plugin-svelte": "^3.15.0",
"jsdom": "25.0.1",
"playwright": "^1.60.0",
"playwright": "^1.62.0",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",

@ -1,5 +1,105 @@
# svelte
## 5.57.0
### Minor Changes
- feat: export `RenderOutput`, `SyncRenderOutput`, `Csp` and `Sha256Source` from `svelte/server` ([#18648](https://github.com/sveltejs/svelte/pull/18648))
- feat: add `has` function to `createContext` ([#18472](https://github.com/sveltejs/svelte/pull/18472))
- feat: support `defaultValue` on `<select>` ([#18591](https://github.com/sveltejs/svelte/pull/18591))
- feat: add getOrInsert/getOrInsertComputed to SvelteMap ([#18728](https://github.com/sveltejs/svelte/pull/18728))
### Patch Changes
- fix: block template store subscriptions on the promise that assigns the store ([#18582](https://github.com/sveltejs/svelte/pull/18582))
- fix: route $derived teardown errors through invoke_error_boundary ([#18486](https://github.com/sveltejs/svelte/pull/18486))
- fix: track SvelteDate snapshots in reactions ([#18700](https://github.com/sveltejs/svelte/pull/18700))
- fix: remove `<svelte:head>` anchors on unmount ([#18697](https://github.com/sveltejs/svelte/pull/18697))
- fix: warn on undeclared shorthand event handlers on `<svelte:window>`, `<svelte:document>` and `<svelte:body>` ([#18480](https://github.com/sveltejs/svelte/pull/18480))
- perf: reuse the cached value in the `<option>`/`<select>` value guard ([#18713](https://github.com/sveltejs/svelte/pull/18713))
- fix: prevent malformed AST output for `<select>` with static `value` attribute ([#18449](https://github.com/sveltejs/svelte/pull/18449))
- fix: apply ownership mutation ignores to binding assignments ([#18718](https://github.com/sveltejs/svelte/pull/18718))
- fix: prevent onoutroend from firing twice when compilerOptions.hmr is true ([#18655](https://github.com/sveltejs/svelte/pull/18655))
- fix: preserve whitespace after inline elements when printing ([#18685](https://github.com/sveltejs/svelte/pull/18685))
- perf: fold SSR block-open markers into the branch's first push ([#18712](https://github.com/sveltejs/svelte/pull/18712))
- fix: run `onDestroy` callbacks when a server render throws ([#18585](https://github.com/sveltejs/svelte/pull/18585))
- fix: report `derived_invalid_export` for `export let x = $derived(...)` in runes mode ([#18692](https://github.com/sveltejs/svelte/pull/18692))
- fix: never apply class hash to elements inside `<svelte:head>` ([#18160](https://github.com/sveltejs/svelte/pull/18160))
- fix: keep `defaultChecked` on hydrated radio inputs with spread attributes ([#18701](https://github.com/sveltejs/svelte/pull/18701))
- fix: accept `onfocusin`/`onfocusout` in `a11y_mouse_events_have_key_events` ([#18689](https://github.com/sveltejs/svelte/pull/18689))
- perf: O(n²)→O(n) Map lookups for legacy `$:` reactive statement ordering ([#18602](https://github.com/sveltejs/svelte/pull/18602))
- fix: distinct memoizer on style/class directives ([#18466](https://github.com/sveltejs/svelte/pull/18466))
- fix: measure nested transitions before applying their starting styles ([#18647](https://github.com/sveltejs/svelte/pull/18647))
- fix: don't turn component instances stored in `$state` into state proxies ([#18646](https://github.com/sveltejs/svelte/pull/18646))
- perf: emit `$.only_child` for elements with a single child ([#18717](https://github.com/sveltejs/svelte/pull/18717))
- fix: omit `bind:focused` from SSR output (it has no HTML attribute) ([#18724](https://github.com/sveltejs/svelte/pull/18724))
- fix: more robust rendering of Svelte custom element slots ([#18710](https://github.com/sveltejs/svelte/pull/18710))
- perf: optimize simple object destructuring in `@const` tags ([#18390](https://github.com/sveltejs/svelte/pull/18390))
- fix: properly apply static textarea value attribute during CSR ([#18727](https://github.com/sveltejs/svelte/pull/18727))
- fix: end a restored reaction context at the end of its synchronous segment ([#18694](https://github.com/sveltejs/svelte/pull/18694))
- fix: keep the dependencies of a reaction that throws, so deriveds it read are neither leaked nor stuck in their error ([#18703](https://github.com/sveltejs/svelte/pull/18703))
- fix: don't resurrect outroing elements when an ancestor block is paused and resumed ([#18431](https://github.com/sveltejs/svelte/pull/18431))
- perf: use `$.comment()` for single-comment templates ([#18714](https://github.com/sveltejs/svelte/pull/18714))
- chore: move `@types/trusted-types` to devDependencies ([#18730](https://github.com/sveltejs/svelte/pull/18730))
- perf: store setters cache as `Set` instead of `Array` ([#18251](https://github.com/sveltejs/svelte/pull/18251))
- fix: transform derived assignments and select function bindings correctly during server-side rendering ([#18669](https://github.com/sveltejs/svelte/pull/18669))
- fix: keep boolean attributes with an empty string value when rendering attribute objects on the server ([#18721](https://github.com/sveltejs/svelte/pull/18721))
- fix: sync `SvelteURL` port signal when the protocol setter clears the port ([#18705](https://github.com/sveltejs/svelte/pull/18705))
- fix: block declaration tags and `{@const}` on async values read inside closures ([#18533](https://github.com/sveltejs/svelte/pull/18533))
- fix: avoid css tree-shaking for exported Snippet ([#18540](https://github.com/sveltejs/svelte/pull/18540))
- perf: treat `<img loading>` as a static element again ([#18711](https://github.com/sveltejs/svelte/pull/18711))
- fix: prevent `selectedcontent` mutation from changing the selected option ([#18495](https://github.com/sveltejs/svelte/pull/18495))
- fix: avoid `NaN` keyframe values in `slide` transition for elements without a layout box ([#18430](https://github.com/sveltejs/svelte/pull/18430))
- fix: preserve line feed character references in attribute values ([#18691](https://github.com/sveltejs/svelte/pull/18691))
- fix: decode uppercase-`X` hex numeric character references (`&#X...;`) ([#18708](https://github.com/sveltejs/svelte/pull/18708))
- chore: clarify when `$effect.pre` runs relative to DOM updates ([#18534](https://github.com/sveltejs/svelte/pull/18534))
- fix: scope SSR boundary failed snippets to their boundary ([#18593](https://github.com/sveltejs/svelte/pull/18593))
## 5.56.10
### Patch Changes

@ -1352,6 +1352,9 @@ export interface HTMLSelectAttributes extends HTMLAttributes<HTMLSelectElement>
required?: boolean | undefined | null;
size?: number | undefined | null;
value?: any;
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: any;
defaultvalue?: any;
'on:change'?: ChangeEventHandler<HTMLSelectElement> | undefined | null;
onchange?: ChangeEventHandler<HTMLSelectElement> | undefined | null;

@ -171,12 +171,6 @@ This can happen if you render a hydratable on the client that was not rendered o
> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
## set_context_after_init
> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## state_descriptors_fixed
> Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.

@ -300,7 +300,7 @@ Enforce that heading elements (`h1`, `h2`, etc.) and anchors have content and th
> '%event%' event must be accompanied by '%accompanied_by%' event
Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` and `onblur`, respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users.
Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` (or `onfocusin`) and `onblur` (or `onfocusout`), respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users.
```svelte
<!-- A11y: onmouseover must be accompanied by onfocus -->

@ -64,7 +64,13 @@ Certain lifecycle methods can only be used during component initialisation. To f
> Context was not set in the current component or any of its ancestors
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
## set_context_after_init
> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## snippet_without_render_tag

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.56.10",
"version": "5.57.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -152,13 +152,14 @@
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.60.0",
"@playwright/test": "^1.62.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"@types/trusted-types": "^2.0.7",
"baseline-browser-mapping": "^2.10.32",
"dts-buddy": "^0.5.5",
"esbuild": "^0.28.1",
@ -170,18 +171,17 @@
"web-features": "^3.29.0"
},
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.10",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"@jridgewell/remapping": "^2.3.5",
"@jridgewell/sourcemap-codec": "^1.6.0",
"@sveltejs/acorn-typescript": "^1.0.13",
"@types/estree": "^1.0.9",
"acorn": "^8.18.0",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.8.1",
"devalue": "^5.9.2",
"esm-env": "^1.2.1",
"esrap": "^2.2.12",
"esrap": "^2.3.6",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -5,6 +5,7 @@ import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import * as e from '../../errors.js';
import { locator } from '../../state.js';
const JSParser = acorn.Parser;
const TSParser = JSParser.extend(tsPlugin());
@ -87,7 +88,8 @@ export function parse_expression_at(parser, source, index) {
sourceType: 'module',
ecmaVersion: 16,
locations: true,
preserveParens: true
preserveParens: true,
startLocation: start_location(parser, index)
});
add_comments(ast);
@ -112,7 +114,13 @@ export function parse_statement_at(parser, source, index) {
try {
// This is like parseExpressionAt but for statements
const p = new acorn(
{ onComment, sourceType: 'module', ecmaVersion: 16, locations: true },
{
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true,
startLocation: start_location(parser, index)
},
source,
index
);
@ -128,6 +136,32 @@ export function parse_statement_at(parser, source, index) {
}
}
const regex_non_lf_line_break = /\r(?!\n)|[\u2028\u2029]/;
let last_template = '';
let lf_only = true;
/**
* Without `startLocation`, acorn counts the lines before `index` on every call
* @param {Parser} parser
* @param {number} index
*/
function start_location(parser, index) {
return has_lf_line_breaks_only(parser) ? locator(index) : undefined;
}
/**
* acorn breaks lines on bare `\r`, `\u2028` and `\u2029`, which the locator doesn't
* @param {Parser} parser
*/
export function has_lf_line_breaks_only(parser) {
if (parser.template !== last_template) {
last_template = parser.template;
lf_only = !regex_non_lf_line_break.test(last_template);
}
return lf_only;
}
const regex_position_indicator = / \(\d+:\d+\)$/;
/**

@ -5,30 +5,11 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
import { create_fragment, disallow_children } from '../nodes.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
/** @param {number} cc */
function is_whitespace(cc) {
// fast path for common whitespace
if (cc === 32 || (cc <= 13 && cc >= 9)) return true;
// rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff
if (cc < 160) return false;
return (
cc === 160 ||
cc === 5760 ||
(cc >= 8192 && cc <= 8202) ||
cc === 8232 ||
cc === 8233 ||
cc === 8239 ||
cc === 8287 ||
cc === 12288 ||
cc === 65279
);
}
import { is_whitespace } from './utils/whitespace.js';
const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
@ -278,8 +259,27 @@ export class Parser {
};
}
/** @param {string} delimiter */
read_until(delimiter) {
if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length);
}
const start = this.index;
const index = this.template.indexOf(delimiter, start);
if (index !== -1) {
this.index = index;
return this.template.slice(start, this.index);
}
this.index = this.template.length;
return this.template.slice(start);
}
/** @param {RegExp} pattern */
read_until(pattern) {
read_until_regex(pattern) {
if (this.index >= this.template.length) {
if (this.loose) return '';
e.unexpected_eof(this.template.length);
@ -302,6 +302,7 @@ export class Parser {
e.expected_whitespace(this.index);
}
this.index++;
this.allow_whitespace();
}

@ -2,7 +2,6 @@
/** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
/**
@ -33,24 +32,10 @@ export default function read_pattern(parser) {
i = match_bracket(parser, start);
parser.index = i;
const pattern_string = parser.template.slice(start, i);
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template
.slice(0, start)
.replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
// acorn never reads before `start`, so the template itself can serve as the prefix
/** @type {any} */
let expression = remove_parens(
parse_expression_at(parser, `${space_with_newline}(${pattern_string} = 1)`, start - 1)
parse_expression_at(parser, parser.template.slice(0, i) + ' = 1', start)
);
expression = expression.left;
@ -80,7 +65,7 @@ function read_type_annotation(parser) {
const insert = '_ as ';
let a = parser.index - insert.length;
const template =
parser.template.slice(0, a).replace(/[^\n]/g, ' ') +
parser.template.slice(0, a) +
insert +
// If this is a type annotation for a function parameter, Acorn-TS will treat subsequent
// parameters as part of a sequence expression instead, and will then error on optional

@ -1,9 +1,13 @@
/** @import { Expression } from 'estree' */
/** @import { Expression, Identifier } from 'estree' */
/** @import { Parser } from '../index.js' */
import { parse_expression_at, remove_parens } from '../acorn.js';
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import { has_lf_line_breaks_only, parse_expression_at, remove_parens } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
import { is_reserved } from '../../../../utils.js';
import { locator } from '../../../state.js';
/**
* @param {Parser} parser
@ -33,6 +37,9 @@ export function get_loose_identifier(parser, opening_token) {
* @returns {Expression}
*/
export default function read_expression(parser, opening_token, disallow_loose) {
const simple = read_simple_expression(parser);
if (simple) return simple;
try {
const node = parse_expression_at(parser, parser.template, parser.index);
@ -57,3 +64,104 @@ export default function read_expression(parser, opening_token, disallow_loose) {
throw err;
}
}
/**
* Most template expressions are an identifier or a `a.b.c` member chain followed by `}`.
* Those are built directly for better parse performance, with the same shape acorn would produce; anything else goes to acorn
* @param {Parser} parser
* @returns {Expression | null}
*/
function read_simple_expression(parser) {
if (!has_lf_line_breaks_only(parser)) return null;
const template = parser.template;
const index = parser.index;
parser.allow_whitespace();
const start = parser.index;
let end = read_word(template, start);
if (end === -1 || is_reserved(template.slice(start, end))) {
parser.index = index;
return null;
}
/** @type {Expression} */
let node = identifier(template, start, end);
while (template[end] === '.') {
const property_end = read_word(template, end + 1);
if (property_end === -1) {
parser.index = index;
return null;
}
node = {
type: 'MemberExpression',
start,
end: property_end,
loc: { start: position(start), end: position(property_end) },
object: node,
property: identifier(template, end + 1, property_end),
computed: false,
optional: false
};
end = property_end;
}
parser.index = end;
parser.allow_whitespace();
if (!parser.match('}')) {
parser.index = index;
return null;
}
parser.index = end;
return node;
}
/**
* @param {string} template
* @param {number} start
* @returns {number} the end of the identifier starting at `start`, or -1
*/
function read_word(template, start) {
if (start >= template.length) return -1;
const code = /** @type {number} */ (template.codePointAt(start));
if (!isIdentifierStart(code, true)) return -1;
let end = start + (code <= 0xffff ? 1 : 2);
while (end < template.length) {
const code = /** @type {number} */ (template.codePointAt(end));
if (!isIdentifierChar(code, true)) break;
end += code <= 0xffff ? 1 : 2;
}
return end;
}
/**
* @param {string} template
* @param {number} start
* @param {number} end
* @returns {Identifier}
*/
function identifier(template, start, end) {
return {
type: 'Identifier',
start,
end,
loc: { start: position(start), end: position(end) },
name: template.slice(start, end)
};
}
/** @param {number} index */
function position(index) {
const { line, column } = locator(index);
return { line, column };
}

@ -22,7 +22,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
*/
export function read_script(parser, start, attributes) {
const script_start = parser.index;
const data = parser.read_until(regex_closing_script_tag);
const data = parser.read_until_regex(regex_closing_script_tag);
if (parser.index >= parser.template.length) {
e.element_unclosed(parser.template.length, 'script');
}

@ -16,8 +16,6 @@ const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
const REGEX_UNICODE_SEQUENCE = /\\[0-9a-fA-F]{1,6}(\r\n|\s)?/y;
const REGEX_COMMENT_CLOSE = /\*\//;
const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
@ -478,7 +476,7 @@ function read_block_item(parser) {
function read_declaration(parser) {
const start = parser.index;
const property = parser.read_until(REGEX_WHITESPACE_OR_COLON);
const property = parser.read_until_regex(REGEX_WHITESPACE_OR_COLON);
parser.allow_whitespace();
parser.eat(':');
let index = parser.index;
@ -661,7 +659,7 @@ function allow_comment_or_whitespace(parser, capture_comments = true) {
}
if (parser.eat('<!--')) {
parser.read_until(REGEX_HTML_COMMENT_CLOSE);
parser.read_until('-->');
parser.eat('-->', true);
}
@ -676,7 +674,7 @@ function allow_comment_or_whitespace(parser, capture_comments = true) {
function read_comment(parser) {
const start = parser.index;
parser.eat('/*', true);
const value = parser.read_until(REGEX_COMMENT_CLOSE);
const value = parser.read_until('*/');
parser.eat('*/', true);
const end = parser.index;

@ -9,19 +9,20 @@ import read_style from '../read/style.js';
import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js';
import {
create_attribute,
create_fragment,
ExpressionMetadata,
is_element_node
} from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
import { locator } from '../../../state.js';
import * as b from '#compiler/builders';
import { is_whitespace } from '../utils/whitespace.js';
const regex_invalid_unquoted_attribute_value = /(\/>|[\s"'=<>`])/y;
const regex_closing_textarea_tag = /<\/textarea(\s[^>]*)?>/iy;
const regex_closing_comment = /-->/;
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
const regex_token_ending_character = /[\s=/>"']/;
const regex_starts_with_quote_characters = /["']/y;
const regex_attribute_value = /(?:"([^"]*)"|'([^'])*'|([^>\s]+))/y;
const regex_doctype_name = /^![a-zA-Z]+$/;
@ -67,7 +68,7 @@ export default function element(parser) {
let parent = parser.current();
if (parser.eat('!--')) {
const data = parser.read_until(regex_closing_comment);
const data = parser.read_until('-->');
parser.eat('-->', true);
parser.append({
@ -81,7 +82,7 @@ export default function element(parser) {
}
if (parser.eat('/')) {
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
const name = read_tag_name(parser);
parser.allow_whitespace();
parser.eat('>', true);
@ -137,7 +138,7 @@ export default function element(parser) {
return;
}
const tag = read_tag(parser, regex_whitespace_or_slash_or_closing_tag);
const tag = read_tag(parser);
if (tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };
@ -475,7 +476,7 @@ function parent_is_shadowroot_template(stack) {
function read_static_attribute(parser) {
const start = parser.index;
const tag = read_tag(parser, regex_token_ending_character);
const tag = read_tag(parser, true);
if (!tag.name) return null;
/** @type {true | Array<AST.Text | AST.ExpressionTag>} */
@ -607,7 +608,7 @@ function read_attribute(parser) {
}
}
const tag = read_tag(parser, regex_token_ending_character);
const tag = read_tag(parser, true);
if (!tag.name) return null;
@ -731,7 +732,7 @@ function read_comment(parser) {
const start = parser.index;
if (parser.eat('//')) {
const value = parser.read_until(/\n/);
const value = parser.read_until('\n');
const end = parser.index;
return {
@ -747,7 +748,7 @@ function read_comment(parser) {
}
if (parser.eat('/*')) {
const value = parser.read_until(/\*\//);
const value = parser.read_until('*/');
parser.eat('*/');
const end = parser.index;
@ -847,25 +848,21 @@ function read_attribute_value(parser) {
* @returns {any[]}
*/
function read_sequence(parser, done, location) {
/** @type {AST.Text} */
let current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
/** @type {Array<AST.Text | AST.ExpressionTag>} */
const chunks = [];
let chunk_start = parser.index;
/** @param {number} end */
function flush(end) {
if (end > current_chunk.start) {
current_chunk.raw = parser.template.slice(current_chunk.start, end);
current_chunk.data = decode_character_references(current_chunk.raw, true);
current_chunk.end = end;
chunks.push(current_chunk);
if (end > chunk_start) {
const raw = parser.template.slice(chunk_start, end);
chunks.push({
start: chunk_start,
end,
type: 'Text',
raw,
data: decode_character_references(raw, true)
});
}
}
@ -879,12 +876,14 @@ function read_sequence(parser, done, location) {
if (parser.match('#')) {
const index = parser.index - 1;
parser.eat('#');
const name = parser.read_until(/[^a-z]/);
// const name = parser.read_until_regex(/[^a-z]/);
const name = read_lowercase_name(parser);
e.block_invalid_placement(index, name, location);
} else if (parser.match('@')) {
const index = parser.index - 1;
parser.eat('@');
const name = parser.read_until(/[^a-z]/);
// const name = parser.read_until_regex(/[^a-z]/);
const name = read_lowercase_name(parser);
e.tag_invalid_placement(index, name, location);
}
@ -907,14 +906,7 @@ function read_sequence(parser, done, location) {
};
chunks.push(chunk);
current_chunk = {
start: parser.index,
end: -1,
type: 'Text',
raw: '',
data: ''
};
chunk_start = parser.index;
} else {
parser.index++;
}
@ -929,12 +921,36 @@ function read_sequence(parser, done, location) {
/**
* @param {Parser} parser
* @param {RegExp} regex
* @param {boolean} [attribute]
*/
function read_tag_name(parser, attribute = false) {
const start = parser.index;
if (start >= parser.template.length && !parser.loose) e.unexpected_eof(parser.template.length);
while (parser.index < parser.template.length) {
const cc = parser.template.charCodeAt(parser.index);
if (
is_whitespace(cc) ||
cc === 47 || // /
cc === 62 || // >
(attribute && (cc === 34 || cc === 39 || cc === 61)) // " ' =
) {
break;
}
parser.index += 1;
}
return parser.template.slice(start, parser.index);
}
/**
* @param {Parser} parser
* @param {boolean} [attribute]
* @returns {Identifier & { start: number, end: number, loc: SourceLocation }}
*/
function read_tag(parser, regex) {
function read_tag(parser, attribute = false) {
const start = parser.index;
const name = parser.read_until(regex);
const name = read_tag_name(parser, attribute);
const end = parser.index;
return {
@ -948,3 +964,15 @@ function read_tag(parser, regex) {
}
};
}
/** @param {Parser} parser */
function read_lowercase_name(parser) {
const start = parser.index;
while (parser.index < parser.template.length) {
const cc = parser.template.charCodeAt(parser.index);
// a-z
if (cc < 97 || cc > 122) break;
parser.index += 1;
}
return parser.template.slice(start, parser.index);
}

@ -3,11 +3,10 @@
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { ExpressionMetadata } from '../../nodes.js';
import { create_fragment, ExpressionMetadata } from '../../nodes.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 { find_matching_bracket, match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /\s*}/y;
@ -488,12 +487,13 @@ function open(parser) {
parser.eat(')', true);
}
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
const params = parser.template.slice(params_start, parser.index);
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(parser, prelude + `${params} => {}`, params_start)
parse_expression_at(
parser,
parser.template.slice(0, parser.index) + ' => {}',
params_start
)
)
: { params: [] };

@ -1,16 +0,0 @@
/** @import { AST } from '#compiler' */
/**
* @param {any} transparent
* @returns {AST.Fragment}
*/
export function create_fragment(transparent = false) {
return {
type: 'Fragment',
nodes: [],
metadata: {
transparent,
dynamic: false
}
};
}

@ -20,7 +20,7 @@ function reg_exp_entity(entity_name, is_attribute_value) {
/** @param {boolean} is_attribute_value */
function get_entity_pattern(is_attribute_value) {
const reg_exp_num = '#(?:x[a-fA-F\\d]+|\\d+)(?:;)?';
const reg_exp_num = '#(?:[xX][a-fA-F\\d]+|\\d+)(?:;)?';
const reg_exp_entities = Object.keys(entities).map(
/** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value)
);
@ -38,6 +38,8 @@ const entity_pattern_attr_value = get_entity_pattern(true);
* @param {boolean} is_attribute_value
*/
export function decode_character_references(html, is_attribute_value) {
if (html.indexOf('&') === -1) return html; // fast path
const entity_pattern = is_attribute_value ? entity_pattern_attr_value : entity_pattern_content;
return html.replace(
entity_pattern,
@ -50,7 +52,7 @@ export function decode_character_references(html, is_attribute_value) {
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x') {
} else if (entity[1] === 'x' || entity[1] === 'X') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
@ -60,7 +62,7 @@ export function decode_character_references(html, is_attribute_value) {
return match;
}
return String.fromCodePoint(validate_code(code));
return String.fromCodePoint(validate_code(code, is_attribute_value));
}
);
}
@ -75,10 +77,15 @@ const NUL = 0;
// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
/** @param {number} code */
function validate_code(code) {
// line feed becomes generic whitespace
if (code === 10) {
/**
* @param {number} code
* @param {boolean} is_attribute_value
*/
function validate_code(code, is_attribute_value) {
// line feed becomes generic whitespace, since it is collapsed along with the
// surrounding whitespace anyway. In an attribute value it is significant, so it
// is left alone there
if (code === 10 && !is_attribute_value) {
return 32;
}

@ -0,0 +1,18 @@
/** @param {number} cc */
export function is_whitespace(cc) {
// fast path for common whitespace
if (cc === 32 || (cc <= 13 && cc >= 9)) return true;
// rare whitespace — \u00a0, \u1680, \u2000-\u200a, \u2028, \u2029, \u202f, \u205f, \u3000, \ufeff
if (cc < 160) return false;
return (
cc === 160 ||
cc === 5760 ||
(cc >= 8192 && cc <= 8202) ||
cc === 8232 ||
cc === 8233 ||
cc === 8239 ||
cc === 8287 ||
cc === 12288 ||
cc === 65279
);
}

@ -145,6 +145,19 @@ export function prune(stylesheet, elements) {
},
ComplexSelector(node) {
const selectors = get_relative_selectors(node);
const rule = /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule);
const first = selectors[0]?.selectors[0];
const is_icss_export =
selectors.length === 1 &&
selectors[0].selectors.length === 1 &&
first?.type === 'PseudoClassSelector' &&
first.name === 'export' &&
first.args === null;
// Global and ICSS export rules do not depend on an element in this component
if (every_is_global(selectors, 0, selectors.length, rule) || is_icss_export) {
node.metadata.used = true;
}
for (const element of elements) {
seen.clear();
@ -153,12 +166,7 @@ export function prune(stylesheet, elements) {
// Elements rendered through <svelte:head> are not style-scopable.
// Prevent css hash injection (class="s-...") on tags like <meta>, <link>, <script>.
!is_inside_svelte_head(element) &&
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element,
BACKWARD
)
apply_selector(selectors, rule, element, BACKWARD)
) {
node.metadata.used = true;
}

@ -14,7 +14,7 @@ import {
} from '../../utils/ast.js';
import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import check_graph_for_cycles from '../../utils/check_graph_for_cycles.js';
import { create_attribute, is_custom_element_node } from '../nodes.js';
import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
@ -285,7 +285,7 @@ export function analyze_module(source, options) {
runes: true,
immutable: true,
tracing: false,
async_deriveds: new Set(),
async_deriveds: new Map(),
comments,
classes: new Map(),
pickled_awaits: new Set()
@ -557,7 +557,7 @@ export function analyze_component(root, source, options) {
source,
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set(),
async_deriveds: new Map(),
pickled_awaits: new Set(),
instance_body: {
sync: [],
@ -832,6 +832,11 @@ export function analyze_component(root, source, options) {
} else {
e.export_undefined(specifier, name);
}
} else if (binding.initial?.type === 'SnippetBlock') {
// If a snippet is exported, a consumer could only import this named export and not the default export (the component).
// In this case we need to set hasGlobal of our output to true so that e.g. vite-plugin-svelte does not tell Vite to
// tree-shake the CSS if the default export is not used.
analysis.css.has_global = true;
}
}
}

@ -1,6 +1,7 @@
/** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler' */
/** @import { ExpressionMetadata } from '../../nodes.js' */
import * as e from '../../../errors.js';
/**
@ -10,15 +11,20 @@ import * as e from '../../../errors.js';
export function AwaitExpression(node, context) {
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
// preserve context for awaits that precede other expressions in template or `$derived(...)`
if (
is_reactive_expression(
context.path,
context.state.derived_function_depth === context.state.function_depth
) &&
!is_last_evaluated_expression(context.path, node)
)
) {
context.state.analysis.pickled_awaits.add(node);
const expression = /** @type {ExpressionMetadata} */ (context.state.expression);
// preserve context for awaits that precede other expressions in template or `$derived(...)`,
// and for any await that follows one, so the restored context ends at the next suspension
if (expression.has_pickled_await || !is_last_evaluated_expression(context.path, node)) {
context.state.analysis.pickled_awaits.add(node);
expression.has_pickled_await = true;
}
}
let suspend = tla;

@ -9,7 +9,7 @@ import { validate_assignment } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js';
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js';
import fuzzymatch from '../../../utils/fuzzymatch.js';
import { is_content_editable_binding, is_svg } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';

@ -253,7 +253,7 @@ export function CallExpression(node, context) {
});
if (expression.has_await) {
context.state.analysis.async_deriveds.add(node);
context.state.analysis.async_deriveds.set(node, expression);
}
// Tell surrounding declaration tag about metadata for correct calculation of blockers etc

@ -1,7 +1,7 @@
/** @import { ClassDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
import { validate_identifier_name } from './shared/utils.js';
import { validate_identifier_name } from '../../scope.js';
/**
* @param {ClassDeclaration} node

@ -43,7 +43,8 @@ export function DeclarationTag(node, context) {
*/
export function mark_async_declaration(context, metadata, declarations) {
const has_await = metadata.expression.has_await;
const blockers = [...metadata.expression.dependencies]
// reads inside closures must block too, like they do in template expressions
const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);

@ -1,4 +1,4 @@
/** @import { ExportNamedDeclaration, Identifier } from 'estree' */
/** @import { ExportNamedDeclaration, Identifier, VariableDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
@ -23,15 +23,6 @@ export function ExportNamedDeclaration(node, context) {
}
if (node.declaration?.type === 'VariableDeclaration') {
// in runes mode, forbid `export let`
if (
context.state.analysis.runes &&
context.state.ast_type === 'instance' &&
node.declaration.kind === 'let'
) {
e.legacy_export_invalid(node);
}
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
const binding = context.state.scope.get(id.name);
@ -46,6 +37,15 @@ export function ExportNamedDeclaration(node, context) {
}
}
}
// in runes mode, forbid `export let`
if (
context.state.analysis.runes &&
context.state.ast_type === 'instance' &&
node.declaration.kind === 'let'
) {
e.legacy_export_invalid(node);
}
}
if (context.state.analysis.runes) {

@ -1,7 +1,7 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import { visit_function } from './shared/function.js';
import { validate_identifier_name } from './shared/utils.js';
import { validate_identifier_name } from '../../scope.js';
/**
* @param {FunctionDeclaration} node

@ -1,12 +1,11 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js';
import { should_proxy, get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js';
/**

@ -2,7 +2,7 @@
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js';
import { disallow_children } from './shared/special-element.js';
import { disallow_children } from '../../nodes.js';
import { check_global_event_reference } from './shared/utils.js';
/**

@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
import { disallow_children } from '../../nodes.js';
import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js';
import { check_global_event_reference } from './shared/utils.js';

@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
import { disallow_children } from '../../nodes.js';
import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js';
import { check_global_event_reference } from './shared/utils.js';

@ -1,8 +1,8 @@
/** @import { Expression, Identifier, Literal, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import { ensure_no_module_import_conflict, validate_identifier_name } from './shared/utils.js';
import { get_rune, validate_identifier_name } from '../../scope.js';
import { ensure_no_module_import_conflict } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_paths } from '../../../utils/ast.js';

@ -4,7 +4,7 @@ import { roles as roles_map, elementRoles } from 'aria-query';
import { AXObjects, elementAXObjects } from 'axobject-query';
export const aria_attributes =
'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(
'activedescendant atomic autocomplete braillelabel brailleroledescription busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(
' '
);

@ -45,7 +45,7 @@ import {
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { list } from '../../../../../utils/string.js';
import { walk } from 'zimmerframe';
import fuzzymatch from '../../../../1-parse/utils/fuzzymatch.js';
import fuzzymatch from '../../../../../utils/fuzzymatch.js';
import { is_content_editable_binding } from '../../../../../../utils.js';
import * as w from '../../../../../warnings.js';
@ -373,11 +373,23 @@ export function check_element(node, context) {
}
}
if (!has_spread && handlers.has('mouseover') && !handlers.has('focus')) {
// `focus`/`blur` don't bubble, so an element that isn't focusable itself can only be notified
// about focus changes inside it through `focusin`/`focusout` — accept those as well
if (
!has_spread &&
handlers.has('mouseover') &&
!handlers.has('focus') &&
!handlers.has('focusin')
) {
w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus');
}
if (!has_spread && handlers.has('mouseout') && !handlers.has('blur')) {
if (
!has_spread &&
handlers.has('mouseout') &&
!handlers.has('blur') &&
!handlers.has('focusout')
) {
w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur');
}

@ -1,16 +0,0 @@
/** @import { AST } from '#compiler' */
import * as e from '../../../../errors.js';
/**
* @param {AST.SvelteBody | AST.SvelteDocument | AST.SvelteOptionsRaw | AST.SvelteWindow} node
*/
export function disallow_children(node) {
const { nodes } = node.fragment;
if (nodes.length > 0) {
const first = nodes[0];
const last = nodes[nodes.length - 1];
e.svelte_meta_invalid_content({ start: first.start, end: last.end }, node.name);
}
}

@ -246,44 +246,6 @@ export function is_pure(node, context) {
return false;
}
/**
* Checks if the name is valid, which it is when it's not starting with (or is) a dollar sign or if it's a function parameter.
* The second argument is the depth of the scope, which is there for backwards compatibility reasons: In Svelte 4, you
* were allowed to define `$`-prefixed variables anywhere below the top level of components. Once legacy mode is gone, this
* argument can be removed / the call sites adjusted accordingly.
* @param {Binding | null} binding
* @param {number | undefined} [function_depth]
*/
export function validate_identifier_name(binding, function_depth) {
if (!binding) return;
const declaration_kind = binding.declaration_kind;
if (
declaration_kind !== 'synthetic' &&
declaration_kind !== 'param' &&
declaration_kind !== 'rest_param' &&
(!function_depth || function_depth <= 1)
) {
const node = binding.node;
if (node.name === '$') {
e.dollar_binding_invalid(node);
} else if (
node.name.startsWith('$') &&
// import type { $Type } from "" - these are normally already filtered out,
// but for the migration they aren't, and throwing here is preventing the migration to complete
// TODO -> once migration script is gone we can remove this check
!(
binding.initial?.type === 'ImportDeclaration' &&
/** @type {any} */ (binding.initial).importKind === 'type'
)
) {
e.dollar_prefix_invalid(node);
}
}
}
/**
* Checks that the exported name is not a derived or reassigned state variable.
* @param {Node} node

@ -39,6 +39,12 @@ export function transform_template(state, name, flags = 0) {
const namespace = state.metadata.namespace;
const tree = state.options.fragments === 'tree';
const { nodes } = state.template;
const is_lone_anchor = nodes.length === 1 && nodes[0].type === 'comment';
// special case - `$.comment` creates the anchor more cheaply than cloning a template
if (is_lone_anchor) return b.id('$.comment');
const expression = tree ? state.template.as_tree() : state.template.as_html();
const key =

@ -2,6 +2,7 @@
/** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { ExpressionMetadata } from '../../nodes.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders';
import { is_simple_expression, save } from '../../../utils/ast.js';
@ -127,57 +128,45 @@ export function is_prop_source(binding, state) {
}
/**
* @param {Expression} node
* @param {Scope | null} scope
* An async thunk. If an `await` inside restores the reaction context via `$.save`,
* the body exits through `$.unsave` so the context cannot leak into foreign microtasks
* that run before the returned promise settles
* @param {Expression | BlockStatement} body
* @param {ExpressionMetadata} metadata
*/
export function should_proxy(node, scope) {
if (
!node ||
node.type === 'Literal' ||
node.type === 'TemplateLiteral' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined')
) {
return false;
export function async_thunk(body, metadata) {
if (!metadata.has_pickled_await) {
return b.arrow([], body, true);
}
if (node.type === 'Identifier' && scope !== null) {
const binding = scope.get(node.name);
// Let's see if the reference is something that can be proxied
if (
binding !== null &&
!binding.reassigned &&
binding.initial !== null &&
binding.initial.type !== 'FunctionDeclaration' &&
binding.initial.type !== 'ClassDeclaration' &&
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock' &&
binding.initial.type !== 'SnippetBlock'
) {
return should_proxy(binding.initial, null);
}
}
const block = body.type === 'BlockStatement' ? body : b.block([b.return(body)]);
return true;
return b.arrow(
[],
b.block([
{
type: 'TryStatement',
block,
handler: null,
finalizer: b.block([b.stmt(b.call('$.unsave'))])
}
]),
true
);
}
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state
* @param {Expression | BlockStatement} expression
* @param {boolean} [async]
* @param {ExpressionMetadata} [metadata]
*/
export function create_derived(state, expression, async = false) {
const thunk = b.thunk(expression, async);
if (async) {
return save(b.call('$.async_derived', thunk));
} else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
export function create_derived(state, expression, metadata) {
if (metadata?.has_await) {
return save(b.call('$.async_derived', async_thunk(expression, metadata)));
}
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', b.thunk(expression));
}
/**

@ -9,10 +9,10 @@ import {
is_expression_async
} from '../../../../utils/ast.js';
import { dev, locate_node } from '../../../../state.js';
import { build_getter, should_proxy } from '../utils.js';
import { build_getter } from '../utils.js';
import { should_proxy, get_rune } from '../../../scope.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/**

@ -1,9 +1,9 @@
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { async_thunk, create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
@ -15,10 +15,10 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(
build_expression(context, node.expression, node.metadata.expression),
node.metadata.expression.has_await
);
const input = build_expression(context, node.expression, node.metadata.expression);
const expression = node.metadata.expression.has_await
? async_thunk(input, node.metadata.expression)
: b.thunk(input);
let then_block;
let catch_block;

@ -1,7 +1,7 @@
/** @import { CallExpression, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import { dev, ignore_map, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { binding_properties } from '../../../bindings.js';
@ -40,9 +40,15 @@ export function BindDirective(node, context) {
validate_binding(context.state, node, expression);
}
const assignment = /** @type {Expression} */ (
context.visit(b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value')))
const raw_assignment = b.assignment(
'=',
/** @type {Pattern} */ (node.expression),
b.id('$$value')
);
// The assignment is generated, so inherit any ignores attached to the binding
ignore_map.set(raw_assignment, ignore_map.get(node) ?? []);
const assignment = /** @type {Expression} */ (context.visit(raw_assignment));
if (dev) {
// in dev, create named functions, so that `$inspect(...)` delivers
@ -58,16 +64,7 @@ export function BindDirective(node, context) {
get = b.thunk(expression);
/** @type {Expression | undefined} */
set = b.unthunk(
b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (
context.visit(
b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value'))
)
)
)
);
set = b.unthunk(b.arrow([b.id('$$value')], assignment));
if (get === set) {
set = undefined;

@ -2,8 +2,7 @@
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js';
import { get_rune, should_proxy } from '../../../scope.js';
import { get_inspect_args } from '../../utils.js';
/**

@ -19,7 +19,7 @@ export function ConstTag(node, context) {
if (declaration.id.type === 'Identifier') {
const init = build_expression(context, declaration.init, node.metadata.expression);
let expression = create_derived(context.state, init, node.metadata.expression.has_await);
let expression = create_derived(context.state, init, node.metadata.expression);
if (dev) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
@ -69,7 +69,7 @@ export function ConstTag(node, context) {
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
]);
let expression = create_derived(context.state, block, node.metadata.expression.has_await);
let expression = create_derived(context.state, block, node.metadata.expression);
if (dev) {
expression = b.call('$.tag', expression, b.literal('[@const]'));

@ -3,6 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { extract_identifiers, has_await_expression } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { add_state_transformers } from './shared/declarations.js';
/**
@ -70,7 +71,7 @@ export function add_async_declaration(context, metadata, ids, assignments, kind
context.state.consts.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
}
const blockers = [...metadata.expression.dependencies]
const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
@ -85,5 +86,5 @@ export function add_async_declaration(context, metadata, ids, assignments, kind
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));
run.thunks.push(has_await ? async_thunk(body, metadata.expression) : b.thunk(body));
}

@ -12,6 +12,7 @@ import {
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
@ -313,7 +314,9 @@ export function EachBlock(node, context) {
const has_await = node.metadata.expression.has_await;
const get_collection = b.thunk(collection, has_await);
const get_collection = has_await
? async_thunk(collection, node.metadata.expression)
: b.thunk(collection);
const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];

@ -141,14 +141,9 @@ export function Fragment(node, context) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template
state.init.unshift(b.var(id, b.call('$.comment')));
} else {
const template_name = transform_template(state, 'root', flags);
const template_name = transform_template(state, 'root', flags);
state.init.unshift(b.var(id, b.call(template_name)));
}
state.init.unshift(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}

@ -2,6 +2,7 @@
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { build_expression } from './shared/utils.js';
/**
@ -46,7 +47,7 @@ export function HtmlTag(node, context) {
'$.async',
context.state.node,
node.metadata.expression.blockers(),
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$html')] : [context.state.node],
b.block([statement])

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
@ -117,7 +118,7 @@ export function IfBlock(node, context) {
'$.async',
context.state.node,
node.metadata.expression.blockers(),
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$condition')] : [context.state.node],
b.block(statements)

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
@ -31,7 +32,7 @@ export function KeyBlock(node, context) {
'$.async',
context.state.node,
node.metadata.expression.blockers(),
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$key')] : [context.state.node],
b.block([statement])

@ -1,4 +1,4 @@
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression } from 'estree' */
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
@ -229,11 +229,18 @@ export function RegularElement(node, context) {
continue;
}
// `<select defaultValue>` needs the options to exist before it can mark one
// as selected, so it is handled after the children, alongside `value`
if (node.name === 'select' && get_attribute_name(node, attribute) === 'defaultValue') {
continue;
}
const name = get_attribute_name(node, attribute);
if (
!is_custom_element &&
!cannot_be_set_statically(attribute.name) &&
(name !== 'value' || node.name !== 'textarea') &&
(attribute.value === true || is_text_attribute(attribute)) &&
(name !== 'class' || class_directives.length === 0) &&
(name !== 'style' || style_directives.length === 0)
@ -432,7 +439,7 @@ export function RegularElement(node, context) {
state: child_state
});
if (needs_reset) {
if (needs_reset && !fold_reset_into_child(child_state.init, context.state.node)) {
child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
}
}
@ -511,6 +518,34 @@ export function RegularElement(node, context) {
}
}
// deferred from the attribute loop above, so that the options it selects from
// have been created and had their values assigned
if (!has_spread && name === 'select') {
const default_value = /** @type {AST.Attribute[]} */ (attributes).find(
(attribute) => get_attribute_name(node, attribute) === 'defaultValue'
);
if (default_value) {
const { value, has_state } = build_attribute_value(default_value.value, context, (v, m) =>
context.state.memoizer.add(v, m)
);
(has_state ? context.state.update : context.state.init).push(
b.stmt(b.call('$.set_default_select_value', node_id, value))
);
}
const value_attribute = lookup.get('value');
const dynamic_value =
value_attribute !== undefined &&
value_attribute.value !== true &&
!is_text_attribute(value_attribute);
if (default_value || dynamic_value || bindings.has('value')) {
context.state.init.push(b.stmt(b.call('$.init_select', node_id)));
}
}
context.state.template.pop_element();
}
@ -701,28 +736,32 @@ function build_element_special_value_attribute(
);
const evaluated = context.state.scope.evaluate(value);
const assignment = b.assignment('=', b.member(node_id, '__value'), value);
const set_value_assignment = b.assignment(
'=',
b.member(node_id, 'value'),
evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal(''))
);
/** @param {Expression} value */
const build_update = (value) => {
const assignment = b.assignment('=', b.member(node_id, '__value'), value);
const update = b.stmt(
is_select_with_value
? b.sequence([
set_value_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.call('$.select_option', node_id, value)
])
: synthetic
? assignment
: set_value_assignment
);
const set_value_assignment = b.assignment(
'=',
b.member(node_id, 'value'),
evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal(''))
);
return b.stmt(
is_select_with_value
? b.sequence([
set_value_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.call('$.select_option', node_id, value)
])
: synthetic
? assignment
: set_value_assignment
);
};
if (has_state) {
const id = b.id(state.scope.generate(`${node_id.name}_value`));
@ -733,12 +772,49 @@ function build_element_special_value_attribute(
const init = element === 'option' ? b.object([]) : undefined;
state.init.push(b.var(id, init));
state.update.push(b.if(b.binary('!==', id, b.assignment('=', id, value)), b.block([update])));
// the guard already evaluated `value` into `id`, so read that back rather than
// evaluating the same expression (and its signal reads) a second time
state.update.push(
b.if(b.binary('!==', id, b.assignment('=', id, value)), b.block([build_update(id)]))
);
} else {
state.init.push(update);
state.init.push(build_update(value));
}
}
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id)));
/**
* `<p>{text}</p>` and friends produce `var x = $.child(p, true); $.reset(p);`. That pair is
* by far the most common shape in compiled output, and `$.only_child` does both, so fold the
* two together when the `$.child(...)` is the last thing we emitted for this element.
* @param {Statement[]} init
* @param {Expression} node_id
* @returns {boolean} whether the reset was folded in
*/
function fold_reset_into_child(init, node_id) {
const last = init.at(-1);
if (
node_id?.type !== 'Identifier' ||
last?.type !== 'VariableDeclaration' ||
last.declarations.length !== 1
) {
return false;
}
const call = last.declarations[0].init;
if (
call?.type !== 'CallExpression' ||
call.callee.type !== 'Identifier' ||
call.callee.name !== '$.child' ||
call.arguments[0]?.type !== 'Identifier' ||
call.arguments[0].name !== node_id.name
) {
return false;
}
call.callee = b.id('$.only_child');
return true;
}

@ -4,6 +4,7 @@
import { dev, locator } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { determine_namespace_for_children } from '../../utils.js';
import {
build_attribute_value,
@ -147,7 +148,7 @@ export function SvelteElement(node, context) {
'$.async',
context.state.node,
node.metadata.expression.blockers(),
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$tag')] : [context.state.node],
b.block(statements)

@ -5,8 +5,8 @@ import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths, save } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
import { get_rune, should_proxy } from '../../../scope.js';
import { async_thunk, get_prop_source, is_prop_source, is_state_source } from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
@ -29,6 +29,7 @@ export function VariableDeclaration(node, context) {
rune === '$inspect' ||
rune === '$inspect.trace' ||
rune === '$state.snapshot' ||
rune === '$state.eager' ||
rune === '$host'
) {
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
@ -200,9 +201,10 @@ export function VariableDeclaration(node, context) {
}
if (rune === '$derived' || rune === '$derived.by') {
const is_async = context.state.analysis.async_deriveds.has(
const metadata = context.state.analysis.async_deriveds.get(
/** @type {CallExpression} */ (init)
);
const is_async = metadata !== undefined;
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (context.visit(value));
@ -213,7 +215,7 @@ export function VariableDeclaration(node, context) {
/** @type {Expression} */
let call = b.call(
'$.async_derived',
b.thunk(expression, true),
async_thunk(expression, metadata),
dev && b.literal(declarator.id.name),
location ? b.literal(location) : undefined
);
@ -246,7 +248,7 @@ export function VariableDeclaration(node, context) {
call = b.call(
'$.async_derived',
b.thunk(expression, true),
async_thunk(expression, metadata),
dev &&
b.literal(
`[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`

@ -30,6 +30,7 @@ export function build_attribute_effect(
) {
/** @type {ObjectExpression['properties']} */
const values = [];
const is_select = element.type === 'RegularElement' && element.name === 'select';
const memoizer = new Memoizer();
@ -48,7 +49,11 @@ export function build_attribute_effect(
context.state.init.push(b.var(id, value));
values.push(b.init(attribute.name, b.id(id)));
} else {
values.push(b.init(attribute.name, value));
const name =
is_select && normalize_attribute(attribute.name) === 'defaultValue'
? 'defaultValue'
: attribute.name;
values.push(b.init(name, value));
}
} else {
let value = /** @type {Expression} */ (context.visit(attribute));

@ -161,7 +161,7 @@ export function is_static_element(node) {
}
if (
['input', 'textarea'].includes(node.name) &&
['input', 'textarea', 'select'].includes(node.name) &&
['value', 'checked'].includes(attribute.name)
) {
return false;
@ -171,11 +171,6 @@ export function is_static_element(node) {
return false;
}
// We need to apply src and loading after appending the img to the DOM for lazy loading to work
if (node.name === 'img' && attribute.name === 'loading') {
return false;
}
if (attribute.value !== true && !is_text_attribute(attribute)) {
return false;
}

@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter, is_state_source } from '../../utils.js';
import { async_thunk, build_getter, is_state_source } from '../../utils.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/**
@ -16,10 +16,10 @@ import { ExpressionMetadata } from '../../../../nodes.js';
* from templates and replacing them with `$0`, `$1` etc
*/
export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
/** @type {Array<{ id: Identifier, expression: Expression, metadata: ExpressionMetadata }>} */
#sync = [];
/** @type {Array<{ id: Identifier, expression: Expression }>} */
/** @type {Array<{ id: Identifier, expression: Expression, metadata: ExpressionMetadata }>} */
#async = [];
/** @type {Set<Expression>} */
@ -43,7 +43,7 @@ export class Memoizer {
const id = b.id('#'); // filled in later
(metadata.has_await ? this.#async : this.#sync).push({ id, expression });
(metadata.has_await ? this.#async : this.#sync).push({ id, expression, metadata });
return id;
}
@ -84,7 +84,7 @@ export class Memoizer {
if (this.#async.length === 0) return;
// use `b.arrow` rather than `b.thunk` so that deferred async/template effects
// always read live bindings rather than a possibly stale snapshot.
return b.array(this.#async.map((memo) => b.arrow([], memo.expression, true)));
return b.array(this.#async.map((memo) => async_thunk(memo.expression, memo.metadata)));
}
sync_values() {

@ -110,7 +110,7 @@ function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right))
);
return b.call(binding.node, value);
return b.call(object, value);
}
return null;

@ -66,7 +66,7 @@ export function add_async_declaration(context, metadata, ids, assignments, kind
context.state.init.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
}
const blockers = [...metadata.expression.dependencies]
const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);

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

Loading…
Cancel
Save