Merge branch 'main' into destroy

pull/10760/head
Rich Harris 2 years ago
commit 44a1670bea

@ -225,6 +225,7 @@
"rotten-buckets-develop",
"rotten-experts-relax",
"rotten-poems-applaud",
"rotten-rules-invite",
"rude-ghosts-tickle",
"selfish-dragons-knock",
"selfish-tools-hide",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: use getters for derived class state fields, with memoisation

@ -1,5 +1,11 @@
# svelte
## 5.0.0-next.75
### Patch Changes
- fix: use getters for derived class state fields, with memoisation ([#10757](https://github.com/sveltejs/svelte/pull/10757))
## 5.0.0-next.74
### Patch Changes

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

@ -17,7 +17,6 @@ export const global_visitors = {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') {
const field = state.private_state.get(node.property.name);
if (field) {
return state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node);
}

@ -546,82 +546,112 @@ const javascript_visitors = {
/** @type {import('./types').Visitors} */
const javascript_visitors_runes = {
ClassBody(node, { state, visit, next }) {
if (!state.analysis.runes) {
next();
}
/** @type {import('estree').PropertyDefinition[]} */
const deriveds = [];
/** @type {import('estree').MethodDefinition | null} */
let constructor = null;
// Get the constructor
ClassBody(node, { state, visit }) {
/** @type {Map<string, import('../../3-transform/client/types.js').StateField>} */
const public_derived = new Map();
/** @type {Map<string, import('../../3-transform/client/types.js').StateField>} */
const private_derived = new Map();
/** @type {string[]} */
const private_ids = [];
for (const definition of node.body) {
if (definition.type === 'MethodDefinition' && definition.kind === 'constructor') {
constructor = /** @type {import('estree').MethodDefinition} */ (visit(definition));
}
}
// Move $derived() runes to the end of the body if there is a constructor
if (constructor !== null) {
const body = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const is_private = definition.key.type === 'PrivateIdentifier';
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, state.scope);
if (rune === '$derived') {
deriveds.push(/** @type {import('estree').PropertyDefinition} */ (visit(definition)));
if (is_private) {
// Keep the private #name initializer if private, but remove initial value
body.push({
...definition,
value: null
});
}
continue;
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const { type, name } = definition.key;
const is_private = type === 'PrivateIdentifier';
if (is_private) private_ids.push(name);
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, state.scope);
if (rune === '$derived' || rune === '$derived.by') {
/** @type {import('../../3-transform/client/types.js').StateField} */
const field = {
kind: rune === '$derived.by' ? 'derived_call' : 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
if (is_private) {
private_derived.set(name, field);
} else {
public_derived.set(name, field);
}
}
}
if (definition.type !== 'MethodDefinition' || definition.kind !== 'constructor') {
body.push(
/** @type {import('estree').PropertyDefinition | import('estree').MethodDefinition | import('estree').StaticBlock} */ (
visit(definition)
)
);
}
}
if (deriveds.length > 0) {
body.push({
...constructor,
value: {
...constructor.value,
body: b.block([
...constructor.value.body.body,
...deriveds.map((d) => {
return b.stmt(
b.assignment(
'=',
b.member(b.this, d.key),
/** @type {import('estree').Expression} */ (d.value)
)
);
})
])
}
// each `foo = $derived()` needs a backing `#foo` field
for (const [name, field] of public_derived) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
/** @type {Array<import('estree').MethodDefinition | import('estree').PropertyDefinition>} */
const body = [];
const child_state = { ...state, private_derived };
// Replace parts of the class body
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const name = definition.key.name;
const is_private = definition.key.type === 'PrivateIdentifier';
const field = (is_private ? private_derived : public_derived).get(name);
if (definition.value?.type === 'CallExpression' && field !== undefined) {
const init = /** @type {import('estree').Expression} **/ (
visit(definition.value.arguments[0], child_state)
);
const value =
field.kind === 'derived_call'
? b.call('$.once', init)
: b.call('$.once', b.thunk(init));
if (is_private) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
if ((field.kind === 'derived' || field.kind === 'derived_call') && state.options.dev) {
body.push(
b.method(
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
);
}
}
});
} else {
body.push(constructor);
continue;
}
}
return {
...node,
body
};
body.push(/** @type {import('estree').MethodDefinition} **/ (visit(definition, child_state)));
}
next();
return { ...node, body };
},
PropertyDefinition(node, { state, next, visit }) {
if (node.value != null && node.value.type === 'CallExpression') {
@ -730,6 +760,16 @@ const javascript_visitors_runes = {
return transform_inspect_rune(node, context);
}
context.next();
},
MemberExpression(node, context) {
if (node.object.type === 'ThisExpression' && node.property.type === 'PrivateIdentifier') {
const field = context.state.private_derived.get(node.property.name);
if (field) {
return b.call(node);
}
}
context.next();
}
};
@ -2089,7 +2129,8 @@ export function server_component(analysis, options) {
metadata: {
namespace: options.namespace
},
preserve_whitespace: options.preserveWhitespace
preserve_whitespace: options.preserveWhitespace,
private_derived: new Map()
};
const module = /** @type {import('estree').Program} */ (
@ -2346,7 +2387,8 @@ export function server_module(analysis, options) {
// this is an anomaly — it can only be used in components, but it needs
// to be present for `javascript_visitors` and so is included in module
// transform state as well as component transform state
legacy_reactive_statements: new Map()
legacy_reactive_statements: new Map(),
private_derived: new Map()
};
const module = /** @type {import('estree').Program} */ (

@ -8,6 +8,7 @@ import type {
import type { SvelteNode, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { StateField } from '../client/types.js';
export type TemplateExpression = {
type: 'expression';
@ -35,6 +36,7 @@ export interface Anchor {
export interface ServerTransformState extends TransformState {
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
readonly private_derived: Map<string, StateField>;
}
export interface ComponentServerTransformState extends ServerTransformState {

@ -121,19 +121,6 @@ export function mutate_store(store, expression, new_value) {
return expression;
}
/**
* @template V
* @param {unknown} val
* @returns {val is import('#client').Store<V>}
*/
export function is_store(val) {
return (
typeof val === 'object' &&
val !== null &&
typeof (/** @type {import('#client').Store<V>} */ (val).subscribe) === 'function'
);
}
/**
* @param {import('#client').Store<number>} store
* @param {number} store_value

@ -8,6 +8,7 @@ import {
is_tag_valid_with_parent
} from '../../constants.js';
import { DEV } from 'esm-env';
import { UNINITIALIZED } from '../client/constants.js';
export * from '../client/validate.js';
@ -666,3 +667,17 @@ export function loop_guard(timeout) {
export function inspect(args, inspect = console.log) {
inspect('init', ...args);
}
/**
* @template V
* @param {() => V} get_value
*/
export function once(get_value) {
let value = /** @type {V} */ (UNINITIALIZED);
return () => {
if (value === UNINITIALIZED) {
value = get_value();
}
return value;
};
}

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

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<p>a\n1</p><p>b\n2</p><p>c\n4</p>`
});

@ -0,0 +1,17 @@
<script>
class Foo {
a = $state(0);
c = $derived(this.b * 2);
b = $derived(this.a * 2);
constructor(a) {
this.a = a;
}
}
const foo = new Foo(1);
</script>
<p>a {foo.a}</p>
<p>b {foo.b}</p>
<p>c {foo.c}</p>

@ -221,7 +221,7 @@ importers:
version: 6.0.0(@codemirror/autocomplete@6.12.0)(@codemirror/lang-css@6.2.1)(@codemirror/lang-html@6.4.8)(@codemirror/lang-javascript@6.2.1)(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.24.0)(@lezer/common@1.2.1)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.13)(@lezer/lr@1.4.0)
'@rich_harris/svelte-split-pane':
specifier: ^1.1.1
version: 1.1.1(svelte@4.2.9)
version: 1.1.1(svelte@packages+svelte)
'@rollup/browser':
specifier: ^3.28.0
version: 3.29.4
@ -242,7 +242,7 @@ importers:
version: 2.0.2
svelte-json-tree:
specifier: ^2.1.0
version: 2.2.0(svelte@4.2.9)
version: 2.2.0(svelte@packages+svelte)
zimmerframe:
specifier: ^1.1.1
version: 1.1.1
@ -252,19 +252,19 @@ importers:
version: 5.0.8
'@sveltejs/adapter-static':
specifier: ^3.0.1
version: 3.0.1(@sveltejs/kit@2.4.3)
version: 3.0.1(@sveltejs/kit@2.5.2)
'@sveltejs/adapter-vercel':
specifier: ^4.0.0
version: 4.0.5(@sveltejs/kit@2.4.3)
specifier: ^5.0.0
version: 5.1.0(@sveltejs/kit@2.5.2)
'@sveltejs/kit':
specifier: ^2.4.3
version: 2.4.3(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.2.9)(vite@5.0.12)
specifier: ^2.5.0
version: 2.5.2(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@packages+svelte)(vite@5.0.12)
'@sveltejs/site-kit':
specifier: 6.0.0-next.59
version: 6.0.0-next.59(@sveltejs/kit@2.4.3)(svelte@4.2.9)
version: 6.0.0-next.59(@sveltejs/kit@2.5.2)(svelte@packages+svelte)
'@sveltejs/vite-plugin-svelte':
specifier: ^3.0.0
version: 3.0.1(svelte@4.2.9)(vite@5.0.12)
version: 3.0.1(svelte@packages+svelte)(vite@5.0.12)
'@types/marked':
specifier: ^6.0.0
version: 6.0.0
@ -281,11 +281,11 @@ importers:
specifier: ^3.1.2
version: 3.1.2(typescript@5.3.3)
svelte:
specifier: ^4.2.0
version: 4.2.9
specifier: workspace:^
version: link:../../packages/svelte
svelte-check:
specifier: ^3.6.3
version: 3.6.3(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.9)
version: 3.6.3(postcss@8.4.35)(svelte@packages+svelte)
tslib:
specifier: ^2.6.2
version: 2.6.2
@ -2187,6 +2187,14 @@ packages:
svelte: 4.2.9
dev: false
/@rich_harris/svelte-split-pane@1.1.1(svelte@packages+svelte):
resolution: {integrity: sha512-y2RRLyrN6DCeIgwA423aAIv/T5JqQeOl2XogBQ/21DvA2IF7oyrLUtXMxmQL2va2NFdeJO6MDx6nDX5X7kau7A==}
peerDependencies:
svelte: ^3.54.0
dependencies:
svelte: link:packages/svelte
dev: false
/@rollup/browser@3.29.4:
resolution: {integrity: sha512-qkWkilNBn+90/9Xn2stuwFpXYhG/mZVPlDkTIPdQSEtJES0NS4o4atceEqeGeHOjQREY2jaIv7ld3IajA/Bmfw==}
dev: false
@ -2448,12 +2456,12 @@ packages:
- utf-8-validate
dev: false
/@sveltejs/adapter-static@3.0.1(@sveltejs/kit@2.4.3):
/@sveltejs/adapter-static@3.0.1(@sveltejs/kit@2.5.2):
resolution: {integrity: sha512-6lMvf7xYEJ+oGeR5L8DFJJrowkefTK6ZgA4JiMqoClMkKq0s6yvsd3FZfCFvX1fQ0tpCD7fkuRVHsnUVgsHyNg==}
peerDependencies:
'@sveltejs/kit': ^2.0.0
dependencies:
'@sveltejs/kit': 2.4.3(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.2.9)(vite@5.0.12)
'@sveltejs/kit': 2.5.2(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@packages+svelte)(vite@5.0.12)
dev: true
/@sveltejs/adapter-vercel@4.0.5(@sveltejs/kit@2.4.3):
@ -2469,6 +2477,19 @@ packages:
- supports-color
dev: true
/@sveltejs/adapter-vercel@5.1.0(@sveltejs/kit@2.5.2):
resolution: {integrity: sha512-Z9yRJ4H2/7LcBlvN2/TKu1H0hWoRGonr8kPhP1GJ23LRW76IbiiX5gs/MLc6+ZGogCZYVJ4USmx6m+RFtvQTRw==}
peerDependencies:
'@sveltejs/kit': ^2.4.0
dependencies:
'@sveltejs/kit': 2.5.2(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@packages+svelte)(vite@5.0.12)
'@vercel/nft': 0.26.2
esbuild: 0.19.11
transitivePeerDependencies:
- encoding
- supports-color
dev: true
/@sveltejs/eslint-config@6.0.4(@typescript-eslint/eslint-plugin@6.21.0)(@typescript-eslint/parser@6.21.0)(eslint-config-prettier@9.1.0)(eslint-plugin-svelte@2.35.1)(eslint-plugin-unicorn@51.0.1)(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-U9pwmDs+DbmsnCgTfu6Bacdwqn0DuI1IQNSiQqTgzVyYfaaj+zy9ZoQCiJfxFBGXHkklyXuRHp0KMx346N0lcQ==}
peerDependencies:
@ -2515,6 +2536,33 @@ packages:
tiny-glob: 0.2.9
vite: 5.0.12(@types/node@20.11.5)(lightningcss@1.23.0)(sass@1.70.0)
/@sveltejs/kit@2.5.2(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@packages+svelte)(vite@5.0.12):
resolution: {integrity: sha512-1Pm2lsBYURQsjnLyZa+jw75eVD4gYHxGRwPyFe4DAmB3FjTVR8vRNWGeuDLGFcKMh/B1ij6FTUrc9GrerogCng==}
engines: {node: '>=18.13'}
hasBin: true
requiresBuild: true
peerDependencies:
'@sveltejs/vite-plugin-svelte': ^3.0.0
svelte: ^4.0.0 || ^5.0.0-next.0
vite: ^5.0.3
dependencies:
'@sveltejs/vite-plugin-svelte': 3.0.1(svelte@packages+svelte)(vite@5.0.12)
'@types/cookie': 0.6.0
cookie: 0.6.0
devalue: 4.3.2
esm-env: 1.0.0
import-meta-resolve: 4.0.0
kleur: 4.1.5
magic-string: 0.30.5
mrmime: 2.0.0
sade: 1.8.1
set-cookie-parser: 2.6.0
sirv: 2.0.4
svelte: link:packages/svelte
tiny-glob: 0.2.9
vite: 5.0.12(@types/node@20.11.5)(lightningcss@1.23.0)(sass@1.70.0)
dev: true
/@sveltejs/repl@0.6.0(@codemirror/lang-html@6.4.8)(@codemirror/search@6.5.6)(@lezer/common@1.2.1)(@lezer/javascript@1.4.13)(@lezer/lr@1.4.0)(@sveltejs/kit@2.4.3)(svelte@4.2.9):
resolution: {integrity: sha512-NADKN0NZhLlSatTSh5CCsdzgf2KHJFRef/8krA/TVWAWos5kSwmZ5fF0UImuqs61Pu/SiMXksaWNTGTiOtr4fQ==}
peerDependencies:
@ -2579,6 +2627,18 @@ packages:
svelte-local-storage-store: 0.6.4(svelte@4.2.9)
dev: true
/@sveltejs/site-kit@6.0.0-next.59(@sveltejs/kit@2.5.2)(svelte@packages+svelte):
resolution: {integrity: sha512-nAUCuunhN0DmurQBxbsauqvdvv4mL0F/Aluxq0hFf6gB3iSn9WdaUZdPMXoujy+8cy+m6UvKuyhkgApZhmOLvw==}
peerDependencies:
'@sveltejs/kit': ^1.20.0
svelte: ^4.0.0
dependencies:
'@sveltejs/kit': 2.5.2(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@packages+svelte)(vite@5.0.12)
esm-env: 1.0.0
svelte: link:packages/svelte
svelte-local-storage-store: 0.6.4(svelte@packages+svelte)
dev: true
/@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.2.9)(vite@5.0.12):
resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==}
engines: {node: ^18.0.0 || >=20}
@ -7692,6 +7752,33 @@ packages:
- sugarss
dev: true
/svelte-check@3.6.3(postcss@8.4.35)(svelte@packages+svelte):
resolution: {integrity: sha512-Q2nGnoysxUnB9KjnjpQLZwdjK62DHyW6nuH/gm2qteFnDk0lCehe/6z8TsIvYeKjC6luKaWxiNGyOcWiLLPSwA==}
hasBin: true
peerDependencies:
svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0
dependencies:
'@jridgewell/trace-mapping': 0.3.22
chokidar: 3.5.3
fast-glob: 3.3.2
import-fresh: 3.3.0
picocolors: 1.0.0
sade: 1.8.1
svelte: link:packages/svelte
svelte-preprocess: 5.1.3(postcss@8.4.35)(svelte@packages+svelte)(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- '@babel/core'
- coffeescript
- less
- postcss
- postcss-load-config
- pug
- sass
- stylus
- sugarss
dev: true
/svelte-eslint-parser@0.33.1(svelte@packages+svelte):
resolution: {integrity: sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -7734,6 +7821,14 @@ packages:
svelte: 4.2.9
dev: false
/svelte-json-tree@2.2.0(svelte@packages+svelte):
resolution: {integrity: sha512-zcfepTrJ6xhpdgRZEujmiFh+ainRw7HO4Bsoh8PMAsm7fkgUPtnrZi3An8tmCFY8jajYhMrauHsd1S1XTeuiCw==}
peerDependencies:
svelte: ^4.0.0
dependencies:
svelte: link:packages/svelte
dev: false
/svelte-local-storage-store@0.4.0(svelte@4.2.9):
resolution: {integrity: sha512-ctPykTt4S3BE5bF0mfV0jKiUR1qlmqLvnAkQvYHLeb9wRyO1MdIFDVI23X+TZEFleATHkTaOpYZswIvf3b2tWA==}
engines: {node: '>=0.14'}
@ -7752,6 +7847,15 @@ packages:
svelte: 4.2.9
dev: true
/svelte-local-storage-store@0.6.4(svelte@packages+svelte):
resolution: {integrity: sha512-45WoY2vSGPQM1sIQJ9jTkPPj20hYeqm+af6mUGRFSPP5WglZf36YYoZqwmZZ8Dt/2SU8lem+BTA8/Z/8TkqNLg==}
engines: {node: '>=0.14'}
peerDependencies:
svelte: ^3.48.0 || >4.0.0
dependencies:
svelte: link:packages/svelte
dev: true
/svelte-preprocess@5.1.3(postcss@8.4.35)(sass@1.70.0)(svelte@4.2.9)(typescript@5.3.3):
resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
engines: {node: '>= 16.0.0', pnpm: ^8.0.0}
@ -7801,6 +7905,54 @@ packages:
typescript: 5.3.3
dev: true
/svelte-preprocess@5.1.3(postcss@8.4.35)(svelte@packages+svelte)(typescript@5.3.3):
resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
engines: {node: '>= 16.0.0', pnpm: ^8.0.0}
requiresBuild: true
peerDependencies:
'@babel/core': ^7.10.2
coffeescript: ^2.5.1
less: ^3.11.3 || ^4.0.0
postcss: ^7 || ^8
postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
pug: ^3.0.0
sass: ^1.26.8
stylus: ^0.55.0
sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0
svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0
typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0'
peerDependenciesMeta:
'@babel/core':
optional: true
coffeescript:
optional: true
less:
optional: true
postcss:
optional: true
postcss-load-config:
optional: true
pug:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
typescript:
optional: true
dependencies:
'@types/pug': 2.0.10
detect-indent: 6.1.0
magic-string: 0.30.5
postcss: 8.4.35
sorcery: 0.11.0
strip-indent: 3.0.0
svelte: link:packages/svelte
typescript: 5.3.3
dev: true
/svelte@4.2.9:
resolution: {integrity: sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==}
engines: {node: '>=16'}

@ -14,8 +14,8 @@
"devDependencies": {
"@fontsource/fira-mono": "^5.0.8",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/adapter-vercel": "^4.0.0",
"@sveltejs/kit": "^2.4.3",
"@sveltejs/adapter-vercel": "^5.0.0",
"@sveltejs/kit": "^2.5.0",
"@sveltejs/site-kit": "6.0.0-next.59",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/marked": "^6.0.0",
@ -24,7 +24,7 @@
"publint": "^0.2.7",
"shiki": "^0.14.7",
"shiki-twoslash": "^3.1.2",
"svelte": "^4.2.0",
"svelte": "workspace:^",
"svelte-check": "^3.6.3",
"tslib": "^2.6.2",
"typescript": "^5.3.3",

@ -133,10 +133,18 @@
{
const { mount, unmount, App, untrack } = __repl_exports;
const console_log = console.log
const console_methods = ['log', 'error', 'trace', 'assert', 'warn', 'table', 'group'];
console.log = function (...v) {
return untrack(() => console_log.apply(this, v));
// The REPL hooks up to the console to provide a virtual console. However, the implementation
// needs to stringify the console to pass over a MessageChannel, which means that the object
// can get deeply read and tracked by accident when using the console. We can avoid this by
// ensuring we untrack the main console methods.
for (const method of console_methods) {
const original = console[method];
console[method] = function (...v) {
return untrack(() => original.apply(this, v));
}
}
const component = mount(App, { target: document.body });
window.__unmount_previous = () => unmount(component);

@ -25,9 +25,16 @@
{/if}
{#if log.level === 'trace' || log.level === 'assert'}
<button class="arrow" class:expand={!log.collapsed} on:click={toggle_group_collapse}>
<span
class="arrow"
role="button"
tabindex="0"
class:expand={!log.collapsed}
on:keyup={toggle_group_collapse}
on:click={toggle_group_collapse}
>
</button>
</span>
{/if}
{#if log.level === 'assert'}

@ -239,79 +239,61 @@ An effect only reruns when the object it reads changes, not when a property insi
<p>{count} doubled is {doubled}</p>
```
You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed.
You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/#H4sIAAAAAAAAE42SzW6DMBCEX2Vl5RDaVCQ9JoDUY--9lUox9lKsGBvZC1GEePcaKPnpqSe86_m0M2t6ViqNnu0_e2Z4jWzP3pqGbRhdmrHwHWrCUHvbOjF2Ei-caijLTU4aCYRtDUEKK0-ccL2NDstNrbRWHoU10t8Eu-121gTVCssSBa3XEaQZ9GMrpziGj0p5OAccCgSHwmEgJZwrNNihg6MyhK7j-gii4uYb_YyGUZ5guQwzPdL7b_U4ZNSOvp9T2B3m1rB5cLx4zMkhtc7AHz7YVCVwEFzrgosTBMuNs52SKDegaPbvWnMH8AhUXaNUIY6-hHCldQhUIcyLCFlfAuHvkCKaYk8iYevGGgy2wyyJnpy9oLwG0sjdNe2yhGhJN32HsUzi2xOapNpl_bSLIYnDeeoVLZE1YI3QSpzSfo7-8J5PKbwOmdf2jC6JZyD7HxpPaMk93aHhF6utVKVCyfbkWhy-hh9Z3o_2nQIAAA==)).
```svelte
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let milliseconds = $state(1000);
$effect(() => {
console.log({ count, doubled });
// This will be recreated whenever `milliseconds` changes
const interval = setInterval(() => {
count += 1;
}, milliseconds);
return () => {
// if a callback is provided, it will run
// a) immediately before the effect re-runs
// b) when the component is destroyed
console.log('cleanup');
clearInterval(interval);
};
});
</script>
<button on:click={() => count++}>
{doubled}
</button>
<h1>{count}</h1>
<p>{count} doubled is {doubled}</p>
<button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button>
```
> `$effect` was designed for managing side effects such as logging or connecting to external systems like third party libraries that have an imperative API. If you're managing state or dataflow, you should use it with caution most of the time, you're better off using a different pattern. Below are some use cases and what to use instead.
### When not to use `$effect`
If you update `$state` inside an `$effect`, you most likely want to use `$derived` instead.
In general, `$effect` is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this...
```svelte
<!-- Don't do this -->
<script>
let count = $state(0);
let doubled = $state();
// don't do this!
$effect(() => {
doubled = count * 2;
});
</script>
<!-- Do this instead: -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
```
This also applies to more complex calculations that require more than a simple expression and write to more than one variable. In these cases, you can use `$derived.by`.
...do this:
```svelte
<!-- Don't do this -->
<script>
let result_1 = $state();
let result_2 = $state();
$effect(() => {
// ... some lengthy code resulting in
result_1 = someValue;
result_2 = someOtherValue;
});
</script>
<!-- Do this instead: -->
<script>
let { result_1, result_2 } = $derived.by(() => {
// ... some lengthy code resulting in
return {
result_1: someValue,
result_2: someOtherValue
};
});
let count = $state(0);
let doubled = $derived(count * 2);
</script>
```
> For things that are more complicated than a simple expression like `count * 2`, you can also use [`$derived.by`](#$derived-by).
When reacting to a state change and writing to a different state as a result, think about if it's possible to use callback props instead.
```svelte

@ -0,0 +1,32 @@
import compiler_cjs from '../../../../../../packages/svelte/compiler.cjs?url';
import package_json from '../../../../../../packages/svelte/package.json?url';
import { read } from '$app/server';
const files = import.meta.glob('../../../../../../packages/svelte/src/**/*.js', {
eager: true,
as: 'url'
});
const prefix = '../../../../../../packages/svelte/';
export const prerender = true;
export function entries() {
const entries = Object.keys(files).map((path) => ({ path: path.replace(prefix, '') }));
entries.push({ path: 'compiler.cjs' }, { path: 'package.json' });
return entries;
}
// service worker requests files under this path to load the compiler and runtime
export async function GET({ params }) {
let url = '';
if (params.path === 'compiler.cjs') {
url = compiler_cjs;
} else if (params.path === 'package.json') {
url = package_json;
} else {
url = files[prefix + params.path];
}
return read(url);
}

@ -1 +0,0 @@
../../../packages/svelte

@ -2,6 +2,12 @@ import adapter from '@sveltejs/adapter-vercel';
/** @type {import('@sveltejs/kit').Config} */
export default {
compilerOptions: {
legacy: {
// site-kit manually instantiates components inside an action
componentApi: true
}
},
kit: {
adapter: adapter({
runtime: 'nodejs18.x'

Loading…
Cancel
Save