simpler-deriveds
Dominic Gannaway 2 weeks ago
commit d19a992769

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure element size bindings don't unsubscribe multiple times from the resize observer

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: prevent misidentification of bindings as delegatable event handlers if used outside event attribute

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: preserve current input values when removing defaults

@ -205,6 +205,7 @@
"good-plums-type",
"good-rivers-yawn",
"good-roses-argue",
"gorgeous-hats-wonder",
"gorgeous-monkeys-carry",
"gorgeous-singers-rest",
"great-fans-unite",
@ -234,6 +235,7 @@
"honest-pans-kick",
"hot-cooks-repair",
"hot-jobs-tap",
"hot-rivers-punch",
"hot-sloths-clap",
"hungry-boxes-relate",
"hungry-dots-fry",
@ -326,6 +328,7 @@
"moody-houses-argue",
"moody-owls-cry",
"moody-sheep-type",
"moody-toys-relax",
"nasty-glasses-begin",
"nasty-lions-double",
"nasty-yaks-peel",
@ -526,6 +529,7 @@
"strong-lemons-provide",
"strong-pans-doubt",
"stupid-parents-crash",
"sweet-bottles-check",
"sweet-mangos-beg",
"sweet-pens-sniff",
"swift-donkeys-perform",
@ -608,6 +612,7 @@
"weak-drinks-speak",
"weak-frogs-bow",
"weak-terms-destroy",
"wet-bats-exercise",
"wet-games-fly",
"wet-pears-remain",
"wet-wombats-repeat",

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: improve runtime performance of capturing reactive signals

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: remove document event listeners on unmount

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: preserve component function context for nested components

@ -60,3 +60,17 @@ jobs:
- name: build and check generated types
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); }
Benchmarks:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm bench
env:
CI: true

2
.gitignore vendored

@ -22,3 +22,5 @@ coverage
.DS_Store
tmp
benchmarking/compare/.results

@ -0,0 +1,24 @@
import { kairo_avoidable } from './benchmarks/kairo/kairo_avoidable.js';
import { kairo_broad } from './benchmarks/kairo/kairo_broad.js';
import { kairo_deep } from './benchmarks/kairo/kairo_deep.js';
import { kairo_diamond } from './benchmarks/kairo/kairo_diamond.js';
import { kairo_mux } from './benchmarks/kairo/kairo_mux.js';
import { kairo_repeated } from './benchmarks/kairo/kairo_repeated.js';
import { kairo_triangle } from './benchmarks/kairo/kairo_triangle.js';
import { kairo_unstable } from './benchmarks/kairo/kairo_unstable.js';
import { mol_bench } from './benchmarks/mol_bench.js';
// This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark)
// Not all tests are the same, and many parts have been tweaked to capture different data.
export const benchmarks = [
kairo_avoidable,
kairo_broad,
kairo_deep,
kairo_diamond,
kairo_triangle,
kairo_mux,
kairo_repeated,
kairo_unstable,
mol_bench
];

@ -0,0 +1,60 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
import { busy } from './util.js';
function setup() {
let head = $.source(0);
let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0));
let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation
let computed4 = $.derived(() => $.get(computed3) + 2);
let computed5 = $.derived(() => $.get(computed4) + 3);
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(computed5);
busy(); // heavy side effect
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(computed5) === 6);
for (let i = 0; i < 1000; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(computed5) === 6);
}
}
};
}
export async function kairo_avoidable() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_avoidable',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,66 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let head = $.source(0);
let last = head;
let counter = 0;
const destroy = $.effect_root(() => {
for (let i = 0; i < 50; i++) {
let current = $.derived(() => {
return $.get(head) + i;
});
let current2 = $.derived(() => {
return $.get(current) + 1;
});
$.render_effect(() => {
$.get(current2);
counter++;
});
last = current2;
}
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
counter = 0
for (let i = 0; i < 50; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(last) === i + 50);
}
assert(counter === 50 * 50);
}
};
}
export async function kairo_broad() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_broad',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,66 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let len = 50;
const iter = 50;
function setup() {
let head = $.source(0);
let current = head;
for (let i = 0; i < len; i++) {
let c = current;
current = $.derived(() => {
return $.get(c) + 1;
});
}
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
counter = 0
for (let i = 0; i < iter; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(current) === len + i);
}
assert(counter === iter);
}
};
}
export async function kairo_deep() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_deep',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,70 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let width = 5;
function setup() {
let head = $.source(0);
let current = [];
for (let i = 0; i < width; i++) {
current.push(
$.derived(() => {
return $.get(head) + 1;
})
);
}
let sum = $.derived(() => {
return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(sum) === 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(sum) === (i + 1) * width);
}
assert(counter === 500);
}
};
}
export async function kairo_diamond() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_diamond',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,63 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let heads = new Array(100).fill(null).map((_) => $.source(0));
const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries());
});
const splited = heads
.map((_, index) => $.derived(() => $.get(mux)[index]))
.map((x) => $.derived(() => $.get(x) + 1));
const destroy = $.effect_root(() => {
splited.forEach((x) => {
$.render_effect(() => {
$.get(x);
});
});
});
return {
destroy,
run() {
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.set(heads[i], i);
});
assert($.get(splited[i]) === i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.set(heads[i], i * 2);
});
assert($.get(splited[i]) === i * 2 + 1);
}
}
};
}
export async function kairo_mux() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_mux',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,67 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let size = 30;
function setup() {
let head = $.source(0);
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < size; i++) {
result += $.get(head);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(current) === size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(current) === i * size);
}
assert(counter === 100);
}
};
}
export async function kairo_repeated() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_repeated',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,80 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let width = 10;
function count(number) {
return new Array(number)
.fill(0)
.map((_, i) => i + 1)
.reduce((x, y) => x + y, 0);
}
function setup() {
let head = $.source(0);
let current = head;
let list = [];
for (let i = 0; i < width; i++) {
let c = current;
list.push(current);
current = $.derived(() => {
return $.get(c) + 1;
});
}
let sum = $.derived(() => {
return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
const constant = count(width);
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(sum) === constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(sum) === constant - width + i * width);
}
assert(counter === 100);
}
};
}
export async function kairo_triangle() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_triangle',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,66 @@
import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let head = $.source(0);
const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < 20; i++) {
result += $.get(head) % 2 ? $.get(double) : $.get(inverse);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(current) === 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.set(head, i);
});
}
assert(counter === 100);
}
};
}
export async function kairo_unstable() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_unstable',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,7 @@
export function busy() {
let a = 0;
for (let i = 0; i < 1_00; i++) {
a++;
}
}

@ -0,0 +1,90 @@
import { assert, fastest_test } from '../utils.js';
import * as $ from '../../packages/svelte/src/internal/client/index.js';
/**
* @param {number} n
*/
function fib(n) {
if (n < 2) return 1;
return fib(n - 1) + fib(n - 2);
}
/**
* @param {number} n
*/
function hard(n) {
return n + fib(16);
}
const numbers = Array.from({ length: 5 }, (_, i) => i);
function setup() {
let res = [];
const A = $.source(0);
const B = $.source(0);
const C = $.derived(() => ($.get(A) % 2) + ($.get(B) % 2));
const D = $.derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2)));
D.equals = function (/** @type {number[]} */ l) {
var r = this.v;
return r !== null && l.length === r.length && l.every((v, i) => v === r[i]);
};
const E = $.derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]));
const F = $.derived(() => hard($.get(D)[0] && $.get(B)));
const G = $.derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0] + $.get(F));
const destroy = $.effect_root(() => {
$.render_effect(() => {
res.push(hard($.get(G)));
});
$.render_effect(() => {
res.push($.get(G));
});
$.render_effect(() => {
res.push(hard($.get(F)));
});
});
return {
destroy,
/**
* @param {number} i
*/
run(i) {
res.length = 0;
$.flush_sync(() => {
$.set(B, 1);
$.set(A, 1 + i * 2);
});
$.flush_sync(() => {
$.set(A, 2 + i * 2);
$.set(B, 2);
});
assert(res[0] === 3198 && res[1] === 1601 && res[2] === 3195 && res[3] === 1598);
}
};
}
export async function mol_bench() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1e4; i++) {
run(i);
}
});
destroy();
return {
benchmark: 'mol_bench',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,90 @@
import fs from 'node:fs';
import path from 'node:path';
import { execSync, fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { benchmarks } from '../benchmarks.js';
// if (execSync('git status --porcelain').toString().trim()) {
// console.error('Working directory is not clean');
// process.exit(1);
// }
const filename = fileURLToPath(import.meta.url);
const runner = path.resolve(filename, '../runner.js');
const outdir = path.resolve(filename, '../.results');
if (fs.existsSync(outdir)) fs.rmSync(outdir, { recursive: true });
fs.mkdirSync(outdir);
const branches = [];
for (const arg of process.argv.slice(2)) {
if (arg.startsWith('--')) continue;
if (arg === filename) continue;
branches.push(arg);
}
if (branches.length === 0) {
branches.push(
execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD').toString().trim()
);
}
if (branches.length === 1) {
branches.push('main');
}
process.on('exit', () => {
execSync(`git checkout ${branches[0]}`);
});
for (const branch of branches) {
console.group(`Benchmarking ${branch}`);
execSync(`git checkout ${branch}`);
await new Promise((fulfil, reject) => {
const child = fork(runner);
child.on('message', (results) => {
fs.writeFileSync(`${outdir}/${branch}.json`, JSON.stringify(results, null, ' '));
fulfil();
});
child.on('error', reject);
});
console.groupEnd();
}
const results = branches.map((branch) => {
return JSON.parse(fs.readFileSync(`${outdir}/${branch}.json`, 'utf-8'));
});
for (let i = 0; i < results[0].length; i += 1) {
console.group(`${results[0][i].benchmark}`);
for (const metric of ['time', 'gc_time']) {
const times = results.map((result) => +result[i][metric]);
let min = Infinity;
let min_index = -1;
for (let b = 0; b < times.length; b += 1) {
if (times[b] < min) {
min = times[b];
min_index = b;
}
}
if (min !== 0) {
console.group(`${metric}: fastest is ${branches[min_index]}`);
times.forEach((time, b) => {
console.log(`${branches[b]}: ${time.toFixed(2)}ms (${((time / min) * 100).toFixed(2)}%)`);
});
console.groupEnd();
}
}
console.groupEnd();
}

@ -0,0 +1,10 @@
import { benchmarks } from '../benchmarks.js';
const results = [];
for (const benchmark of benchmarks) {
const result = await benchmark();
console.error(result.benchmark);
results.push(result);
}
process.send(results);

@ -0,0 +1,32 @@
import * as $ from '../packages/svelte/src/internal/client/index.js';
import { benchmarks } from './benchmarks.js';
let total_time = 0;
let total_gc_time = 0;
// eslint-disable-next-line no-console
console.log('-- Benchmarking Started --');
$.push({}, true);
try {
for (const benchmark of benchmarks) {
const results = await benchmark();
// eslint-disable-next-line no-console
console.log(results);
total_time += Number(results.time);
total_gc_time += Number(results.gc_time);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error('-- Benchmarking Failed --');
// eslint-disable-next-line no-console
console.error(e);
process.exit(1);
}
$.pop();
// eslint-disable-next-line no-console
console.log('-- Benchmarking Complete --');
// eslint-disable-next-line no-console
console.log({
total_time: total_time.toFixed(2),
total_gc_time: total_gc_time.toFixed(2)
});

@ -0,0 +1,17 @@
{
"compilerOptions": {
"moduleResolution": "Bundler",
"target": "ESNext",
"module": "ESNext",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"checkJs": true
},
"include": ["./run.js", "./utils.js", "./benchmarks"]
}

@ -0,0 +1,98 @@
import { performance, PerformanceObserver } from 'node:perf_hooks';
import v8 from 'v8-natives';
// Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking.
class GarbageTrack {
track_id = 0;
observer = new PerformanceObserver((list) => this.perf_entries.push(...list.getEntries()));
perf_entries = [];
periods = [];
watch(fn) {
this.track_id++;
const start = performance.now();
const result = fn();
const end = performance.now();
this.periods.push({ track_id: this.track_id, start, end });
return { result, track_id: this.track_id };
}
/**
* @param {number} track_id
*/
async gcDuration(track_id) {
await promise_delay(10);
const period = this.periods.find((period) => period.track_id === track_id);
if (!period) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject('no period found');
}
const entries = this.perf_entries.filter(
(e) => e.startTime >= period.start && e.startTime < period.end
);
return entries.reduce((t, e) => e.duration + t, 0);
}
destroy() {
this.observer.disconnect();
}
constructor() {
this.observer.observe({ entryTypes: ['gc'] });
}
}
function promise_delay(timeout = 0) {
return new Promise((resolve) => setTimeout(resolve, timeout));
}
/**
* @param {{ (): void; (): any; }} fn
*/
function run_timed(fn) {
const start = performance.now();
const result = fn();
const time = performance.now() - start;
return { result, time };
}
/**
* @param {() => void} fn
*/
async function run_tracked(fn) {
v8.collectGarbage();
const gc_track = new GarbageTrack();
const { result: wrappedResult, track_id } = gc_track.watch(() => run_timed(fn));
const gc_time = await gc_track.gcDuration(track_id);
const { result, time } = wrappedResult;
gc_track.destroy();
return { result, timing: { time, gc_time } };
}
/**
* @param {number} times
* @param {() => void} fn
*/
export async function fastest_test(times, fn) {
const results = [];
for (let i = 0; i < times; i++) {
const run = await run_tracked(fn);
results.push(run);
}
const fastest = results.reduce((a, b) => (a.timing.time < b.timing.time ? a : b));
return fastest;
}
/**
* @param {boolean} a
*/
export function assert(a) {
if (!a) {
throw new Error('Assertion failed');
}
}

@ -23,7 +23,10 @@
"test": "vitest run",
"test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
"changeset:version": "changeset version && pnpm -r generate:version && git add --all",
"changeset:publish": "changeset publish"
"changeset:publish": "changeset publish",
"bench": "node --allow-natives-syntax ./benchmarking/run.js",
"bench:compare": "node --allow-natives-syntax ./benchmarking/compare/index.js",
"bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
@ -41,6 +44,7 @@
"prettier-plugin-svelte": "^3.1.2",
"typescript": "^5.3.3",
"typescript-eslint": "^8.0.0-alpha.20",
"v8-natives": "^1.2.5",
"vitest": "^1.2.1"
},
"pnpm": {

@ -1,5 +1,23 @@
# svelte
## 5.0.0-next.160
### Patch Changes
- chore: improve runtime performance of capturing reactive signals ([#12093](https://github.com/sveltejs/svelte/pull/12093))
## 5.0.0-next.159
### Patch Changes
- fix: ensure element size bindings don't unsubscribe multiple times from the resize observer ([#12091](https://github.com/sveltejs/svelte/pull/12091))
- fix: prevent misidentification of bindings as delegatable event handlers if used outside event attribute ([#12081](https://github.com/sveltejs/svelte/pull/12081))
- fix: preserve current input values when removing defaults ([#12083](https://github.com/sveltejs/svelte/pull/12083))
- fix: preserve component function context for nested components ([#12089](https://github.com/sveltejs/svelte/pull/12089))
## 5.0.0-next.158
### Patch Changes

@ -113,6 +113,7 @@ export interface DOMAttributes<T extends EventTarget> {
'on:beforeinput'?: EventHandler<InputEvent, T> | undefined | null;
onbeforeinput?: EventHandler<InputEvent, T> | undefined | null;
onbeforeinputcapture?: EventHandler<InputEvent, T> | undefined | null;
// oninput can be either an InputEvent or an Event, depending on the target element (input, textarea etc).
'on:input'?: FormEventHandler<T> | undefined | null;
oninput?: FormEventHandler<T> | undefined | null;
oninputcapture?: FormEventHandler<T> | undefined | null;

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.158",
"version": "5.0.0-next.160",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -111,23 +111,24 @@ function get_delegated_event(event_name, handler, context) {
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent == null) {
return non_hoistable;
}
if (parent == null) return non_hoistable;
const grandparent = path.at(-2);
/** @type {import('#compiler').RegularElement | null} */
let element = null;
/** @type {string | null} */
let event_name = null;
if (parent.type === 'OnDirective') {
element = /** @type {import('#compiler').RegularElement} */ (path.at(-2));
element = /** @type {import('#compiler').RegularElement} */ (grandparent);
event_name = parent.name;
} else if (
parent.type === 'ExpressionTag' &&
is_event_attribute(/** @type {import('#compiler').Attribute} */ (path.at(-2)))
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
element = /** @type {import('#compiler').RegularElement} */ (path.at(-3));
const attribute = /** @type {import('#compiler').Attribute} */ (path.at(-2));
const attribute = /** @type {import('#compiler').Attribute} */ (grandparent);
event_name = get_attribute_event_name(attribute.name);
}

@ -886,7 +886,12 @@ function serialize_inline_component(node, component_name, context) {
if (slot_name === 'default' && !has_children_prop) {
push_prop(
b.init('children', context.state.options.dev ? b.call('$.wrap_snippet', slot_fn) : slot_fn)
b.init(
'children',
context.state.options.dev
? b.call('$.wrap_snippet', slot_fn, b.id(context.state.analysis.name))
: slot_fn
)
);
// We additionally add the default slot as a boolean, so that the slot render function on the other
// side knows it should get the content to render from $$props.children
@ -2030,7 +2035,7 @@ export const template_visitors = {
}
if (needs_input_reset && node.name === 'input') {
context.state.init.push(b.stmt(b.call('$.remove_input_attr_defaults', context.state.node)));
context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node)));
}
if (needs_content_reset && node.name === 'textarea') {
@ -2699,7 +2704,7 @@ export const template_visitors = {
let snippet = b.arrow(args, body);
if (context.state.options.dev) {
snippet = b.call('$.wrap_snippet', snippet);
snippet = b.call('$.wrap_snippet', snippet, b.id(context.state.analysis.name));
}
const declaration = b.var(node.expression, snippet);

@ -39,15 +39,13 @@ export function snippet(get_snippet, node, ...args) {
* In development, wrap the snippet function so that it passes validation, and so that the
* correct component context is set for ownership checks
* @param {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} fn
* @returns
* @param {any} component
*/
export function wrap_snippet(fn) {
let component = /** @type {import('#client').ComponentContext} */ (current_component_context);
export function wrap_snippet(fn, component) {
return add_snippet_symbol(
(/** @type {import('#client').TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component.function);
set_dev_current_component_function(component);
try {
return fn(node, ...args);

@ -16,29 +16,40 @@ import { queue_idle_task, queue_micro_task } from '../task.js';
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
* @param {HTMLInputElement} dom
* @param {HTMLInputElement} input
* @returns {void}
*/
export function remove_input_attr_defaults(dom) {
if (hydrating) {
let already_removed = false;
// We try and remove the default attributes later, rather than sync during hydration.
// Doing it sync during hydration has a negative impact on performance, but deferring the
// work in an idle task alleviates this greatly. If a form reset event comes in before
// the idle callback, then we ensure the input defaults are cleared just before.
const remove_defaults = () => {
if (already_removed) return;
already_removed = true;
const value = dom.getAttribute('value');
set_attribute(dom, 'value', null);
set_attribute(dom, 'checked', null);
if (value) dom.value = value;
};
// @ts-expect-error
dom.__on_r = remove_defaults;
queue_idle_task(remove_defaults);
add_form_reset_listener();
}
export function remove_input_defaults(input) {
if (!hydrating) return;
var already_removed = false;
// We try and remove the default attributes later, rather than sync during hydration.
// Doing it sync during hydration has a negative impact on performance, but deferring the
// work in an idle task alleviates this greatly. If a form reset event comes in before
// the idle callback, then we ensure the input defaults are cleared just before.
var remove_defaults = () => {
if (already_removed) return;
already_removed = true;
// Remove the attributes but preserve the values
if (input.hasAttribute('value')) {
var value = input.value;
set_attribute(input, 'value', null);
input.value = value;
}
if (input.hasAttribute('checked')) {
var checked = input.checked;
set_attribute(input, 'checked', null);
input.checked = checked;
}
};
// @ts-expect-error
input.__on_r = remove_defaults;
queue_idle_task(remove_defaults);
add_form_reset_listener();
}
/**

@ -1,4 +1,5 @@
import { effect, teardown } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
/**
* Resize observer singleton.
@ -100,7 +101,8 @@ export function bind_element_size(element, type, update) {
var unsub = resize_observer_border_box.observe(element, () => update(element[type]));
effect(() => {
update(element[type]);
// The update could contain reads which should be ignored
untrack(() => update(element[type]));
return unsub;
});
}

@ -21,7 +21,7 @@ export { element } from './dom/blocks/svelte-element.js';
export { head } from './dom/blocks/svelte-head.js';
export { action } from './dom/elements/actions.js';
export {
remove_input_attr_defaults,
remove_input_defaults,
set_attribute,
set_attributes,
set_custom_element_data,

@ -251,6 +251,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
return () => {
for (const event_name of registered_events) {
target.removeEventListener(event_name, bound_event_listener);
document.removeEventListener(event_name, bound_event_listener);
}
root_event_handles.delete(event_handle);
mounted_components.delete(component);

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.158';
export const VERSION = '5.0.0-next.160';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,16 @@
import { test } from '../../test';
export default test({
server_props: {
name: 'server'
},
props: {
name: 'browser'
},
test(assert, target) {
const input = target.querySelector('input');
assert.equal(input?.value, 'browser');
}
});

@ -0,0 +1,5 @@
<script>
const { name } = $props();
</script>
<input type="text" value={name} />

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
test() {
// Compiler shouldn't error
}
});

@ -0,0 +1,5 @@
<script>
function f() {}
</script>
<input onchange={f}>{f}

@ -0,0 +1,5 @@
<script>
let { children } = $props();
</script>
{@render children()}

@ -0,0 +1,5 @@
<script>
let { children } = $props();
</script>
{@render children()}

@ -0,0 +1,5 @@
<script lang="ts">
let { count = $bindable() } = $props();
</script>
<button onclick={() => count.value++}>{count.value}</button>

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
// Tests that nested snippets preserve correct component function context so we don't get false positive warnings
export default test({
html: `<button>0</button>`,
compileOptions: {
dev: true
},
test({ assert, target, warnings }) {
const button = target.querySelector('button');
button?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
assert.deepEqual(warnings, []);
},
warnings: []
});

@ -0,0 +1,13 @@
<script>
import Component1 from './Component1.svelte';
import Component2 from './Component2.svelte';
import Component3 from './Component3.svelte';
let count = $state({ value: 0 });
</script>
<Component1>
<Component2>
<Component3 bind:count></Component3>
</Component2>
</Component1>

@ -16,11 +16,11 @@ export default function State_proxy_literal($$anchor) {
var fragment = root();
var input = $.first_child(fragment);
$.remove_input_attr_defaults(input);
$.remove_input_defaults(input);
var input_1 = $.sibling($.sibling(input, true));
$.remove_input_attr_defaults(input_1);
$.remove_input_defaults(input_1);
var button = $.sibling($.sibling(input_1, true));
@ -30,4 +30,4 @@ export default function State_proxy_literal($$anchor) {
$.append($$anchor, fragment);
}
$.delegate(["click"]);
$.delegate(["click"]);

@ -16,7 +16,7 @@ importers:
version: 2.27.1
'@sveltejs/eslint-config':
specifier: ^7.0.1
version: 7.0.1(@stylistic/eslint-plugin-js@1.8.0(eslint@9.0.0))(eslint-config-prettier@9.1.0(eslint@9.0.0))(eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.144))(eslint-plugin-unicorn@52.0.0(eslint@9.0.0))(eslint@9.0.0)(typescript-eslint@8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3))(typescript@5.3.3)
version: 7.0.1(@stylistic/eslint-plugin-js@1.8.0(eslint@9.0.0))(eslint-config-prettier@9.1.0(eslint@9.0.0))(eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.158))(eslint-plugin-unicorn@52.0.0(eslint@9.0.0))(eslint@9.0.0)(typescript-eslint@8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3))(typescript@5.3.3)
'@svitejs/changesets-changelog-github-compact':
specifier: ^1.1.0
version: 1.1.0
@ -49,13 +49,16 @@ importers:
version: 3.2.4
prettier-plugin-svelte:
specifier: ^3.1.2
version: 3.1.2(prettier@3.2.4)(svelte@5.0.0-next.144)
version: 3.1.2(prettier@3.2.4)(svelte@5.0.0-next.158)
typescript:
specifier: ^5.3.3
version: 5.3.3
typescript-eslint:
specifier: ^8.0.0-alpha.20
version: 8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3)
v8-natives:
specifier: ^1.2.5
version: 1.2.5
vitest:
specifier: ^1.2.1
version: 1.2.1(@types/node@20.11.5)(jsdom@22.0.0)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
@ -4575,8 +4578,8 @@ packages:
resolution: {integrity: sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==}
engines: {node: '>=16'}
svelte@5.0.0-next.144:
resolution: {integrity: sha512-akjtRBHzaLa1XdMv9tBGkXE5N2JaRc3gL+ZIctjc9Gew9DF7NxGTlxXq+HR9yUV7Lsg4o9ltMfkxz8H3K7piNQ==}
svelte@5.0.0-next.158:
resolution: {integrity: sha512-QRmXxHByWntyWqLtzjNsBbNT89F2yA7aWPp9M9l9a6/PAE3gmQh6+qoVPgrxMR7iiFgpwh5ZU9Bm25j3IhGicQ==}
engines: {node: '>=18'}
symbol-tree@3.2.4:
@ -4829,6 +4832,10 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
v8-natives@1.2.5:
resolution: {integrity: sha512-CVNliz6KF2yet3HBIkbFJKZmjlt95C8dsNZDnwoS6X98+QJRpsSz9uxo3TziBqdyJQkWwfD3VG9lRzsQNvF24Q==}
engines: {node: '>= 0.6.0'}
v8-to-istanbul@9.2.0:
resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==}
engines: {node: '>=10.12.0'}
@ -6453,12 +6460,12 @@ snapshots:
- encoding
- supports-color
'@sveltejs/eslint-config@7.0.1(@stylistic/eslint-plugin-js@1.8.0(eslint@9.0.0))(eslint-config-prettier@9.1.0(eslint@9.0.0))(eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.144))(eslint-plugin-unicorn@52.0.0(eslint@9.0.0))(eslint@9.0.0)(typescript-eslint@8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3))(typescript@5.3.3)':
'@sveltejs/eslint-config@7.0.1(@stylistic/eslint-plugin-js@1.8.0(eslint@9.0.0))(eslint-config-prettier@9.1.0(eslint@9.0.0))(eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.158))(eslint-plugin-unicorn@52.0.0(eslint@9.0.0))(eslint@9.0.0)(typescript-eslint@8.0.0-alpha.20(eslint@9.0.0)(typescript@5.3.3))(typescript@5.3.3)':
dependencies:
'@stylistic/eslint-plugin-js': 1.8.0(eslint@9.0.0)
eslint: 9.0.0
eslint-config-prettier: 9.1.0(eslint@9.0.0)
eslint-plugin-svelte: 2.38.0(eslint@9.0.0)(svelte@5.0.0-next.144)
eslint-plugin-svelte: 2.38.0(eslint@9.0.0)(svelte@5.0.0-next.158)
eslint-plugin-unicorn: 52.0.0(eslint@9.0.0)
globals: 15.0.0
typescript: 5.3.3
@ -7570,7 +7577,7 @@ snapshots:
eslint-plugin-lube@0.4.3: {}
eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.144):
eslint-plugin-svelte@2.38.0(eslint@9.0.0)(svelte@5.0.0-next.158):
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.0.0)
'@jridgewell/sourcemap-codec': 1.4.15
@ -7584,9 +7591,9 @@ snapshots:
postcss-safe-parser: 6.0.0(postcss@8.4.38)
postcss-selector-parser: 6.0.16
semver: 7.6.0
svelte-eslint-parser: 0.35.0(svelte@5.0.0-next.144)
svelte-eslint-parser: 0.35.0(svelte@5.0.0-next.158)
optionalDependencies:
svelte: 5.0.0-next.144
svelte: 5.0.0-next.158
transitivePeerDependencies:
- supports-color
- ts-node
@ -9094,10 +9101,10 @@ snapshots:
prettier: 3.2.4
svelte: 4.2.9
prettier-plugin-svelte@3.1.2(prettier@3.2.4)(svelte@5.0.0-next.144):
prettier-plugin-svelte@3.1.2(prettier@3.2.4)(svelte@5.0.0-next.158):
dependencies:
prettier: 3.2.4
svelte: 5.0.0-next.144
svelte: 5.0.0-next.158
prettier@2.8.8: {}
@ -9726,7 +9733,7 @@ snapshots:
- stylus
- sugarss
svelte-eslint-parser@0.35.0(svelte@5.0.0-next.144):
svelte-eslint-parser@0.35.0(svelte@5.0.0-next.158):
dependencies:
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
@ -9734,7 +9741,7 @@ snapshots:
postcss: 8.4.38
postcss-scss: 4.0.9(postcss@8.4.38)
optionalDependencies:
svelte: 5.0.0-next.144
svelte: 5.0.0-next.158
svelte-hmr@0.16.0(svelte@4.2.9):
dependencies:
@ -9809,7 +9816,7 @@ snapshots:
magic-string: 0.30.5
periscopic: 3.1.0
svelte@5.0.0-next.144:
svelte@5.0.0-next.158:
dependencies:
'@ampproject/remapping': 2.2.1
'@jridgewell/sourcemap-codec': 1.4.15
@ -10070,6 +10077,8 @@ snapshots:
utils-merge@1.0.1: {}
v8-natives@1.2.5: {}
v8-to-istanbul@9.2.0:
dependencies:
'@jridgewell/trace-mapping': 0.3.22

Loading…
Cancel
Save