Merge branch 'main' into gh-12624

gh-12624
Rich Harris 7 months ago
commit 6e08f0cb53

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: always return true from `deleteProperty` trap

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: throw error if derived creates state and then depends on it

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure assignments to state field inside constructor trigger effects

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure $inspect works with SvelteMap and SvelteSet

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle deletions of previously-unread state proxy properties

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly handle proxied array length mutations

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: default options.filename to "(unknown)"

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly transform destructured `$derived.by` declarations

@ -21,6 +21,7 @@
"beige-cobras-smoke",
"beige-flies-wash",
"beige-gifts-appear",
"beige-lamps-ring",
"beige-mirrors-listen",
"beige-rabbits-shave",
"beige-seas-share",
@ -82,6 +83,7 @@
"clean-cats-wave",
"clean-eels-beg",
"clean-melons-wash",
"clean-shirts-yawn",
"clever-chefs-relate",
"clever-maps-travel",
"clever-rockets-burn",
@ -198,6 +200,7 @@
"fifty-rice-wait",
"fifty-steaks-float",
"fifty-toys-invite",
"five-maps-reflect",
"five-tigers-search",
"flat-feet-visit",
"flat-ghosts-fly",
@ -273,12 +276,14 @@
"great-fans-unite",
"great-icons-retire",
"great-plums-pretend",
"green-baboons-sip",
"green-eggs-approve",
"green-fishes-lie",
"green-hounds-play",
"green-snails-tickle",
"green-tigers-judge",
"green-walls-clap",
"green-windows-tap",
"grumpy-avocados-fetch",
"grumpy-insects-sleep",
"grumpy-jars-sparkle",
@ -419,6 +424,7 @@
"mighty-files-hammer",
"mighty-frogs-obey",
"mighty-paws-smash",
"mighty-poets-fix",
"mighty-shoes-nail",
"modern-apricots-promise",
"modern-fishes-double",
@ -478,6 +484,7 @@
"olive-cobras-wonder",
"olive-forks-grin",
"olive-kangaroos-brake",
"olive-llamas-warn",
"olive-mice-fix",
"olive-moons-act",
"olive-seals-sell",
@ -538,6 +545,7 @@
"quiet-cobras-smile",
"quiet-crabs-nail",
"quiet-timers-speak",
"rare-ears-agree",
"rare-insects-tell",
"rare-mirrors-act",
"rare-pears-whisper",
@ -606,6 +614,7 @@
"shiny-rats-heal",
"shiny-shrimps-march",
"shiny-starfishes-cross",
"shiny-wombats-argue",
"short-buses-camp",
"short-countries-rush",
"short-starfishes-beg",
@ -628,6 +637,7 @@
"six-boats-shave",
"six-chicken-kneel",
"six-gorillas-obey",
"six-moons-invent",
"six-vans-add",
"sixty-items-crash",
"sixty-numbers-hope",
@ -820,7 +830,9 @@
"weak-frogs-bow",
"weak-terms-destroy",
"wet-bats-exercise",
"wet-donkeys-fry",
"wet-games-fly",
"wet-pears-buy",
"wet-pears-remain",
"wet-wombats-repeat",
"wicked-bikes-matter",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly hydrate empty raw blocks

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make internal sources ownerless

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: repair `href` attribute mismatches

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: join text nodes separated by comments

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow non-synchronous legacy component instantiation

@ -2,12 +2,12 @@
# see: https://github.com/sveltejs/svelte/pull/9609
documentation/docs/05-misc/03-typescript.md
# The following are all duplicated with prettierignore configs in the packages,
# which is necessary because of https://github.com/prettier/prettier-vscode/issues/3424
packages/**/dist/*.js
packages/**/build/*.js
packages/**/npm/**/*
packages/**/config/*.js
# packages/svelte
packages/svelte/messages/**/*.md
packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.js
@ -17,6 +17,7 @@ packages/svelte/src/internal/shared/errors.js
packages/svelte/src/internal/shared/warnings.js
packages/svelte/src/internal/server/errors.js
packages/svelte/tests/migrate/samples/*/output.svelte
packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected*
packages/svelte/tests/**/_actual*
packages/svelte/tests/**/expected*
@ -29,6 +30,7 @@ packages/svelte/compiler/index.js
playgrounds/sandbox/input/**.svelte
playgrounds/sandbox/output
# sites/svelte.dev
sites/svelte.dev/static/svelte-app.json
sites/svelte.dev/scripts/svelte-app/
sites/svelte.dev/src/routes/_components/Supporters/contributors.jpg

@ -1,22 +1,6 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Playground: Browser",
"url": "http://localhost:10001"
},
{
"type": "node",
"request": "launch",
"runtimeArgs": ["--watch"],
"name": "Playground: Server",
"outputCapture": "std",
"program": "start.js",
"cwd": "${workspaceFolder}/playgrounds/demo",
"cascadeTerminateToConfigurations": ["Playground: Browser"]
},
{
"type": "node",
"request": "launch",
@ -26,11 +10,5 @@
"NODE_OPTIONS": "--stack-trace-limit=10000"
}
}
],
"compounds": [
{
"name": "Playground: Full",
"configurations": ["Playground: Server", "Playground: Browser"]
}
]
}

@ -3,7 +3,7 @@ import * as $ from '../../../packages/svelte/src/internal/client/index.js';
import { busy } from './util.js';
function setup() {
let head = $.source(0);
let head = $.state(0);
let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0));
let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let head = $.source(0);
let head = $.state(0);
let last = head;
let counter = 0;

@ -5,7 +5,7 @@ let len = 50;
const iter = 50;
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = head;
for (let i = 0; i < len; i++) {
let c = current;

@ -4,7 +4,7 @@ import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let width = 5;
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = [];
for (let i = 0; i < width; i++) {
current.push(

@ -2,7 +2,7 @@ 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));
let heads = new Array(100).fill(null).map((_) => $.state(0));
const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries());
});

@ -4,7 +4,7 @@ import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let size = 30;
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < size; i++) {

@ -11,7 +11,7 @@ function count(number) {
}
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = head;
let list = [];
for (let i = 0; i < width; i++) {

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() {
let head = $.source(0);
let head = $.state(0);
const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => {

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

@ -20,8 +20,8 @@ const numbers = Array.from({ length: 5 }, (_, i) => i);
function setup() {
let res = [];
const A = $.source(0);
const B = $.source(0);
const A = $.state(0);
const B = $.state(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) {

@ -9,7 +9,7 @@ const COUNT = 1e5;
*/
function create_data_signals(n, sources) {
for (let i = 0; i < n; i++) {
sources[i] = $.source(i);
sources[i] = $.state(i);
}
return sources;
}

@ -33,6 +33,8 @@ To run _side-effects_ when the component is mounted to the DOM, and when values
The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
You can place `$effect` anywhere, not just at the top level of a component, as long as it is called during component initialization (or while a parent effect is active). It is then tied to the lifecycle of the component (or parent effect) and will therefore destroy itself when the component unmounts (or the parent effect 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

@ -39,7 +39,8 @@ export default [
{
languageOptions: {
parserOptions: {
project: true
projectService: true,
tsconfigRootDir: import.meta.dirname
}
},
plugins: {
@ -64,10 +65,14 @@ export default [
}
},
{
files: ['playgrounds/**/*'],
// If you get an error along the lines of "@typescript-eslint/await-thenable needs a project service configured", then that likely means
// that eslint rules that need to be type-aware run through a Svelte file which seems unsupported at the moment. In that case, ensure that
// these are excluded to run on Svelte files.
files: ['**/*.svelte'],
rules: {
'lube/svelte-naming-convention': 'off',
'no-console': 'off'
'@typescript-eslint/await-thenable': 'off',
'@typescript-eslint/prefer-promise-reject-errors': 'off',
'@typescript-eslint/require-await': 'off'
}
},
{
@ -87,6 +92,12 @@ export default [
'packages/svelte/src/internal/client/warnings.js',
'packages/svelte/src/internal/shared/warnings.js',
'packages/svelte/compiler/index.js',
// stuff we don't want to lint
'benchmarking/**',
'coverage/**',
'playgrounds/sandbox/**',
// exclude top level config files
'*.config.js',
// documentation can contain invalid examples
'documentation',
// contains a fork of the REPL which doesn't adhere to eslint rules

@ -18,8 +18,8 @@
"build:sites": "pnpm -r --filter=./sites/* build",
"preview-site": "npm run build --prefix sites/svelte-5-preview",
"check": "cd packages/svelte && pnpm build && cd ../../ && pnpm -r check",
"lint": "pnpm -r lint && prettier --check documentation",
"format": "pnpm -r format && prettier --check --write documentation",
"lint": "eslint && prettier --check .",
"format": "prettier --write .",
"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",
@ -33,18 +33,18 @@
"@sveltejs/eslint-config": "^8.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@vitest/coverage-v8": "^1.2.1",
"eslint": "^9.6.0",
"@vitest/coverage-v8": "^2.0.5",
"eslint": "^9.9.1",
"eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0",
"playwright": "^1.41.1",
"jsdom": "25.0.0",
"playwright": "^1.46.1",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "workspace:^",
"typescript": "^5.5.2",
"typescript-eslint": "^8.0.0-alpha.34",
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0",
"v8-natives": "^1.2.5",
"vitest": "^1.2.1"
"vitest": "^2.0.5"
},
"pnpm": {
"overrides": {

@ -1,22 +0,0 @@
dist/*.js
build/*.js
npm/**/*
config/*.js
messages/**/*.md
src/compiler/errors.js
src/compiler/warnings.js
src/internal/client/errors.js
src/internal/client/warnings.js
src/internal/shared/errors.js
src/internal/shared/warnings.js
src/internal/server/errors.js
tests/**/*.svelte
tests/**/_expected*
tests/**/_actual*
tests/**/expected*
tests/**/_output
tests/**/shards/*.test.js
tests/hydration/samples/*/_expected.html
tests/hydration/samples/*/_override.html
types
compiler/index.js

@ -1,5 +1,45 @@
# svelte
## 5.0.0-next.238
### Patch Changes
- fix: always return true from `deleteProperty` trap ([#13008](https://github.com/sveltejs/svelte/pull/13008))
- fix: handle deletions of previously-unread state proxy properties ([#13008](https://github.com/sveltejs/svelte/pull/13008))
- fix: make internal sources ownerless ([#13013](https://github.com/sveltejs/svelte/pull/13013))
- fix: join text nodes separated by comments ([#13009](https://github.com/sveltejs/svelte/pull/13009))
## 5.0.0-next.237
### Patch Changes
- breaking: throw error if derived creates state and then depends on it ([#12985](https://github.com/sveltejs/svelte/pull/12985))
- fix: ensure assignments to state field inside constructor trigger effects ([#12985](https://github.com/sveltejs/svelte/pull/12985))
- fix: ensure $inspect works with SvelteMap and SvelteSet ([#12994](https://github.com/sveltejs/svelte/pull/12994))
- chore: default options.filename to "(unknown)" ([#12997](https://github.com/sveltejs/svelte/pull/12997))
- feat: allow non-synchronous legacy component instantiation ([#12970](https://github.com/sveltejs/svelte/pull/12970))
## 5.0.0-next.236
### Patch Changes
- fix: properly transform destructured `$derived.by` declarations ([#12984](https://github.com/sveltejs/svelte/pull/12984))
## 5.0.0-next.235
### Patch Changes
- chore: update client check for smaller bundle size ([#12975](https://github.com/sveltejs/svelte/pull/12975))
- fix: correctly hydrate empty raw blocks ([#12979](https://github.com/sveltejs/svelte/pull/12979))
## 5.0.0-next.234
### Patch Changes

@ -0,0 +1,18 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": [
"src/*/index.js",
"src/index-client.ts",
"src/index-server.ts",
"src/index.d.ts",
"tests/**/*.js",
"tests/**/*.ts",
"!tests/**/*.svelte",
"!tests/**/*.svelte.js",
"!tests/**/_output",
"!tests/runtime-browser/driver.js",
"!tests/runtime-browser/driver-ssr.js",
"!tests/types/component.ts"
],
"project": ["src/**"]
}

@ -72,10 +72,10 @@
> Cannot set prototype of `$state` object
## state_unsafe_mutation
## state_unsafe_local_read
> Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state`
> Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
## svelte_component_invalid_this_value
## state_unsafe_mutation
> The `this={...}` property of a `<svelte:component>` must be a Svelte component, if defined
> Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state`

@ -302,10 +302,6 @@ HTML restricts where certain elements can appear. In case of a violation the bro
> `<svelte:fragment>` must be the direct child of a component
## svelte_fragment_invalid_slot
> `<svelte:fragment>` slot attribute must have a static value
## svelte_head_illegal_attribute
> `<svelte:head>` cannot have attributes nor directives

@ -1,7 +1,3 @@
## derived_iife
> Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`
## export_let_unused
> Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.234",
"version": "5.0.0-next.238",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -112,50 +112,38 @@
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
"prepublishOnly": "pnpm build",
"format": "prettier --check --write .",
"lint": "prettier --check . && eslint",
"knip": "pnpm dlx knip"
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.22",
"@playwright/test": "^1.35.1",
"@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.46.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"dts-buddy": "^0.5.1",
"esbuild": "^0.19.11",
"rollup": "^4.9.5",
"esbuild": "^0.21.5",
"rollup": "^4.21.0",
"source-map": "^0.7.4",
"tiny-glob": "^0.2.9"
"tiny-glob": "^0.2.9",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
},
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@types/estree": "^1.0.5",
"acorn": "^8.11.3",
"acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.0",
"axobject-query": "^4.0.0",
"axobject-query": "^4.1.0",
"esm-env": "^1.0.0",
"esrap": "^1.2.2",
"is-reference": "^3.0.2",
"locate-character": "^3.0.0",
"magic-string": "^0.30.5",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"
},
"knip": {
"entry": [
"src/*/index.js",
"src/index-client.ts",
"src/index-server.ts",
"src/index.d.ts",
"tests/**/*.js",
"tests/**/*.ts"
],
"project": [
"src/**"
]
}
}

@ -1246,15 +1246,6 @@ export function svelte_fragment_invalid_placement(node) {
e(node, "svelte_fragment_invalid_placement", "`<svelte:fragment>` must be the direct child of a component");
}
/**
* `<svelte:fragment>` slot attribute must have a static value
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_fragment_invalid_slot(node) {
e(node, "svelte_fragment_invalid_slot", "`<svelte:fragment>` slot attribute must have a static value");
}
/**
* `<svelte:head>` cannot have attributes nor directives
* @param {null | number | NodeLike} node

@ -106,7 +106,7 @@ export function compileModule(source, options) {
*/
export function parse(source, { filename, rootDir, modern } = {}) {
state.reset_warning_filter(() => false);
state.reset(source, { filename, rootDir }); // TODO it's weird to require filename/rootDir here. reconsider the API
state.reset(source, { filename: filename ?? '(unknown)', rootDir });
const ast = _parse(source);
return to_public_ast(source, ast, modern);

@ -243,7 +243,7 @@ export function analyze_module(ast, options) {
return {
module: { ast, scope, scopes },
name: options.filename || 'module',
name: options.filename,
accessors: false,
runes: true,
immutable: true
@ -349,7 +349,7 @@ export function analyze_component(root, source, options) {
}
}
const component_name = get_component_name(options.filename ?? 'Component');
const component_name = get_component_name(options.filename);
const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune);
@ -390,7 +390,7 @@ export function analyze_component(root, source, options) {
hash: root.css
? options.cssHash({
css: root.css.content.styles,
filename: options.filename ?? '<unknown>',
filename: options.filename,
name: component_name,
hash
})

@ -207,7 +207,7 @@ export function client_component(analysis, options) {
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'legacy_reactive') {
legacy_reactive_declarations.push(b.const(name, b.call('$.mutable_source')));
legacy_reactive_declarations.push(b.const(name, b.call('$.mutable_state')));
}
if (binding.kind === 'store_sub') {
if (store_setup.length === 0) {
@ -505,14 +505,12 @@ export function client_component(analysis, options) {
}
if (dev) {
if (filename) {
// add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later
body.unshift(
b.stmt(
b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
)
);
}
// add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later
body.unshift(
b.stmt(
b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
)
);
body.unshift(b.stmt(b.call(b.id('$.mark_module_start'))));
body.push(b.stmt(b.call(b.id('$.mark_module_end'), b.id(analysis.name))));

@ -47,19 +47,10 @@ export function build_getter(node, state) {
/**
* @param {Expression} value
* @param {PrivateIdentifier | string} proxy_reference
* @param {Expression} previous
*/
export function build_proxy_reassignment(value, proxy_reference) {
return dev
? b.call(
'$.proxy',
value,
b.null,
typeof proxy_reference === 'string'
? b.id(proxy_reference)
: b.member(b.this, proxy_reference)
)
: b.call('$.proxy', value);
export function build_proxy_reassignment(value, previous) {
return dev ? b.call('$.proxy', value, b.null, previous) : b.call('$.proxy', value);
}
/**

@ -48,7 +48,7 @@ function build_assignment(operator, left, right, context) {
value =
private_state.kind === 'raw_state'
? value
: build_proxy_reassignment(value, private_state.id);
: build_proxy_reassignment(value, b.member(b.this, private_state.id));
}
if (!context.state.in_constructor) {
@ -57,20 +57,6 @@ function build_assignment(operator, left, right, context) {
return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value);
}
}
} else if (left.property.type === 'Identifier' && context.state.in_constructor) {
const public_state = context.state.public_state.get(left.property.name);
if (public_state !== undefined && should_proxy(right, context.state.scope)) {
const value = /** @type {Expression} */ (context.visit(right));
return b.assignment(
operator,
/** @type {Pattern} */ (context.visit(left)),
public_state.kind === 'raw_state'
? value
: build_proxy_reassignment(value, public_state.id)
);
}
}
}
@ -109,7 +95,7 @@ function build_assignment(operator, left, right, context) {
context.state.analysis.runes &&
should_proxy(value, context.state.scope)
) {
value = binding.kind === 'raw_state' ? value : build_proxy_reassignment(value, object.name);
value = binding.kind === 'raw_state' ? value : build_proxy_reassignment(value, object);
}
return transform.assign(object, value);

@ -113,17 +113,17 @@ export function ClassBody(node, context) {
value =
field.kind === 'state'
? b.call(
'$.source',
'$.state',
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
)
: field.kind === 'raw_state'
? b.call('$.source', init)
? b.call('$.state', init)
: field.kind === 'derived_by'
? b.call('$.derived', init)
: b.call('$.derived', b.thunk(init));
} else {
// if no arguments, we know it's state as `$derived()` is a compile error
value = b.call('$.source');
value = b.call('$.state');
}
if (is_private) {
@ -139,12 +139,14 @@ export function ClassBody(node, context) {
if (field.kind === 'state') {
// set foo(value) { this.#foo = value; }
const value = b.id('value');
const prev = b.member(b.this, field.id);
body.push(
b.method(
'set',
definition.key,
[value],
[b.stmt(b.call('$.set', member, build_proxy_reassignment(value, field.id)))]
[b.stmt(b.call('$.set', member, build_proxy_reassignment(value, prev)))]
)
);
}

@ -13,15 +13,6 @@ export function MemberExpression(node, context) {
if (field) {
return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node);
}
} else if (node.object.type === 'ThisExpression') {
// rewrite `this.foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'Identifier' && !node.computed) {
const field = context.state.public_state.get(node.property.name);
if (field && context.state.in_constructor) {
return b.member(b.member(b.this, field.id), 'v');
}
}
}
context.next();

@ -126,7 +126,7 @@ export function VariableDeclaration(node, context) {
value = b.call('$.proxy', value);
}
if (is_state_source(binding, context.state.analysis)) {
value = b.call('$.source', value);
value = b.call('$.state', value);
}
return value;
};
@ -173,7 +173,7 @@ export function VariableDeclaration(node, context) {
let id;
let rhs = value;
if (init.arguments[0].type === 'Identifier') {
if (rune === '$derived' && init.arguments[0].type === 'Identifier') {
id = init.arguments[0];
} else {
id = b.id(context.state.scope.generate('$$d'));
@ -291,7 +291,7 @@ export function VariableDeclaration(node, context) {
*/
function create_state_declarators(declarator, scope, value) {
if (declarator.id.type === 'Identifier') {
return [b.declarator(declarator.id, b.call('$.mutable_source', value))];
return [b.declarator(declarator.id, b.call('$.mutable_state', value))];
}
const tmp = scope.generate('tmp');
@ -303,7 +303,7 @@ function create_state_declarators(declarator, scope, value) {
const binding = scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
binding?.kind === 'state' ? b.call('$.mutable_state', value) : value
);
})
];

@ -60,9 +60,9 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
* @param {Sequence} sequence
*/
function flush_sequence(sequence) {
if (sequence.length === 1 && sequence[0].type === 'Text') {
if (sequence.every((node) => node.type === 'Text')) {
skipped += 1;
state.template.push(sequence[0].raw);
state.template.push(sequence.map((node) => node.raw).join(''));
return;
}

@ -80,7 +80,7 @@ export function transform_module(analysis, source, options) {
? server_module(analysis, options)
: client_module(analysis, options);
const basename = (options.filename ?? 'Module').split(/[/\\]/).at(-1);
const basename = options.filename.split(/[/\\]/).at(-1);
if (program.body.length > 0) {
program.body[0].leadingComments = [
{

@ -356,7 +356,7 @@ export function server_component(analysis, options) {
body.push(b.export_default(component_function));
}
if (dev && filename) {
if (dev) {
// add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later
body.unshift(
b.stmt(

@ -8,9 +8,9 @@ import { getLocator } from 'locate-character';
export let warnings = [];
/**
* The filename (if specified in the compiler options) relative to the rootDir (if specified).
* The filename relative to the rootDir (if specified).
* This should not be used in the compiler output except in dev mode
* @type {string | undefined}
* @type {string}
*/
export let filename;
@ -77,20 +77,16 @@ export function is_ignored(node, code) {
/**
* @param {string} _source
* @param {{ dev?: boolean; filename?: string; rootDir?: string }} options
* @param {{ dev?: boolean; filename: string; rootDir?: string }} options
*/
export function reset(_source, options) {
source = _source;
const root_dir = options.rootDir?.replace(/\\/g, '/');
filename = options.filename?.replace(/\\/g, '/');
filename = options.filename.replace(/\\/g, '/');
dev = !!options.dev;
if (
typeof filename === 'string' &&
typeof root_dir === 'string' &&
filename.startsWith(root_dir)
) {
if (typeof root_dir === 'string' && filename.startsWith(root_dir)) {
// make filename relative to rootDir
filename = filename.replace(root_dir, '').replace(/^[/\\]/, '');
}

@ -56,7 +56,7 @@ export interface CompileError extends ICompileDiagnostic {}
export type CssHashGetter = (args: {
name: string;
filename: string | undefined;
filename: string;
css: string;
hash: (input: string) => string;
}) => string;
@ -219,11 +219,7 @@ export interface ModuleCompileOptions {
// The following two somewhat scary looking types ensure that certain types are required but can be undefined still
export type ValidatedModuleCompileOptions = Omit<
Required<ModuleCompileOptions>,
'filename' | 'rootDir'
> & {
filename: ModuleCompileOptions['filename'];
export type ValidatedModuleCompileOptions = Omit<Required<ModuleCompileOptions>, 'rootDir'> & {
rootDir: ModuleCompileOptions['rootDir'];
};

@ -352,7 +352,7 @@ export function prop(kind, key, value, computed = false) {
* @returns {ESTree.PropertyDefinition}
*/
export function prop_def(key, value, computed = false, is_static = false) {
return { type: 'PropertyDefinition', key, value, computed, static: is_static };
return { type: 'PropertyDefinition', key, value, computed, static: is_static, decorators: [] };
}
/**
@ -541,7 +541,8 @@ export function method(kind, key, params, body, computed = false, is_static = fa
kind,
value: function_builder(null, params, block(body)),
computed,
static: is_static
static: is_static,
decorators: []
};
}

@ -328,7 +328,7 @@ function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_inp
}
}
});
return /** @type {SourceMap} */ (result_map);
return /** @type {any} */ (result_map);
}
const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/;
// parse attached sourcemap in processed.code
@ -393,7 +393,7 @@ export function parse_attached_sourcemap(processed, tag_name) {
*/
export function merge_with_preprocessor_map(result, options, source_name) {
if (options.sourcemap) {
const file_basename = get_basename(options.filename || 'input.svelte');
const file_basename = get_basename(options.filename);
// The preprocessor map is expected to contain `sources: [basename_of_filename]`, but our own
// map may contain a different file name. Patch our map beforehand to align sources so merging
// with the preprocessor map works correctly.
@ -442,11 +442,10 @@ export function get_basename(filename) {
}
/**
* @param {string | undefined} filename
* @param {string} filename
* @param {string | undefined} output_filename
* @param {string} fallback
*/
export function get_source_name(filename, output_filename, fallback) {
if (!filename) return fallback;
return output_filename ? get_relative_path(output_filename, filename) : get_basename(filename);
}

@ -9,7 +9,7 @@ import * as w from './warnings.js';
*/
const common = {
filename: string(undefined),
filename: string('(unknown)'),
// default to process.cwd() where it exists to replicate svelte4 behavior
// see https://github.com/sveltejs/svelte/blob/b62fc8c8fd2640c9b99168f01b9d958cb2f7574f/packages/svelte/src/compiler/compile/Component.js#L211

@ -94,7 +94,6 @@ export const codes = [
"options_removed_hydratable",
"options_removed_loop_guard_timeout",
"options_renamed_ssr_dom",
"derived_iife",
"export_let_unused",
"legacy_component_creation",
"non_reactive_update",
@ -574,14 +573,6 @@ export function options_renamed_ssr_dom(node) {
w(node, "options_renamed_ssr_dom", "`generate: \"dom\"` and `generate: \"ssr\"` options have been renamed to \"client\" and \"server\" respectively");
}
/**
* Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`
* @param {null | NodeLike} node
*/
export function derived_iife(node) {
w(node, "derived_iife", "Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`");
}
/**
* Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`
* @param {null | NodeLike} node

@ -17,6 +17,8 @@ export interface ComponentConstructorOptions<
context?: Map<any, any>;
hydrate?: boolean;
intro?: boolean;
recover?: boolean;
sync?: boolean;
$$inline?: boolean;
}

@ -52,10 +52,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {Effect | null} */
var catch_effect;
var input_source = runes
? source(/** @type {V} */ (undefined))
: mutable_source(/** @type {V} */ (undefined));
var error_source = runes ? source(undefined) : mutable_source(undefined);
var input_source = (runes ? source : mutable_source)(/** @type {V} */ (undefined));
var error_source = (runes ? source : mutable_source)(undefined);
var resolved = false;
/**

@ -47,15 +47,20 @@ export function html(node, get_value, svg, mathml, skip_warning) {
var value = '';
/** @type {Effect | null} */
/** @type {Effect | undefined} */
var effect;
block(() => {
if (value === (value = get_value())) return;
if (value === (value = get_value())) {
if (hydrating) {
hydrate_next();
}
return;
}
if (effect) {
if (effect !== undefined) {
destroy_effect(effect);
effect = null;
effect = undefined;
}
if (value === '') return;

@ -90,7 +90,11 @@ export function set_attribute(element, attribute, value, skip_warning) {
if (hydrating) {
attributes[attribute] = element.getAttribute(attribute);
if (attribute === 'src' || attribute === 'href' || attribute === 'srcset') {
if (
attribute === 'src' ||
attribute === 'srcset' ||
(attribute === 'href' && element.nodeName === 'LINK')
) {
if (!skip_warning) {
check_src_in_dev_hydration(element, attribute, value);
}
@ -407,7 +411,7 @@ function check_src_in_dev_hydration(element, attribute, value) {
w.hydration_attribute_changed(
attribute,
element.outerHTML.replace(element.innerHTML, '...'),
element.outerHTML.replace(element.innerHTML, element.innerHTML && '...'),
String(value)
);
}

@ -310,11 +310,7 @@ export function apply(
handler.apply(element, args);
} else if (has_side_effects || handler != null) {
const filename = component?.[FILENAME];
const location = filename
? loc
? ` at ${filename}:${loc[0]}:${loc[1]}`
: ` in ${filename}`
: '';
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
const event_name = args[0].type;
const description = `\`${event_name}\` handler${location}`;

@ -311,33 +311,33 @@ export function state_prototype_fixed() {
}
/**
* Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state`
* Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
* @returns {never}
*/
export function state_unsafe_mutation() {
export function state_unsafe_local_read() {
if (DEV) {
const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived is forbidden. If the value should not be reactive, declare it without \`$state\``);
const error = new Error(`state_unsafe_local_read\nReading state that was created inside the same derived is forbidden. Consider using \`untrack\` to read locally created state`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("state_unsafe_mutation");
throw new Error("state_unsafe_local_read");
}
}
/**
* The `this={...}` property of a `<svelte:component>` must be a Svelte component, if defined
* Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state`
* @returns {never}
*/
export function svelte_component_invalid_this_value() {
export function state_unsafe_mutation() {
if (DEV) {
const error = new Error(`svelte_component_invalid_this_value\nThe \`this={...}\` property of a \`<svelte:component>\` must be a Svelte component, if defined`);
const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived is forbidden. If the value should not be reactive, declare it without \`$state\``);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("svelte_component_invalid_this_value");
throw new Error("state_unsafe_mutation");
}
}

@ -106,7 +106,7 @@ export {
user_effect,
user_pre_effect
} from './reactivity/effects.js';
export { mutable_source, mutate, source, set } from './reactivity/sources.js';
export { mutable_state, mutate, set, state } from './reactivity/sources.js';
export {
prop,
rest_props,

@ -19,7 +19,7 @@ import * as e from './errors.js';
* @param {T} value
* @param {ProxyMetadata | null} [parent]
* @param {Source<T>} [prev] dev mode only
* @returns {ProxyStateObject<T> | T}
* @returns {T}
*/
export function proxy(value, parent = null, prev) {
// if non-proxyable, or is already a proxy, return `value`
@ -33,10 +33,17 @@ export function proxy(value, parent = null, prev) {
return value;
}
/** @type {Map<any, Source<any>>} */
var sources = new Map();
var is_proxied_array = is_array(value);
var version = source(0);
if (is_proxied_array) {
// We need to create the length source eagerly to ensure that
// mutations to the array are properly synced with our proxy
sources.set('length', source(/** @type {any[]} */ (value).length));
}
/** @type {ProxyMetadata} */
var metadata;
@ -91,17 +98,17 @@ export function proxy(value, parent = null, prev) {
deleteProperty(target, prop) {
var s = sources.get(prop);
var exists = s !== undefined ? s.v !== UNINITIALIZED : prop in target;
if (s !== undefined) {
if (s === undefined) {
if (prop in target) {
sources.set(prop, source(UNINITIALIZED));
}
} else {
set(s, UNINITIALIZED);
}
if (exists) {
update_version(version);
}
return exists;
return true;
},
get(target, prop, receiver) {
@ -187,6 +194,22 @@ export function proxy(value, parent = null, prev) {
var s = sources.get(prop);
var has = prop in target;
// variable.length = value -> clear all signals with index >= value
if (is_proxied_array && prop === 'length') {
for (var i = value; i < /** @type {Source<number>} */ (s).v; i += 1) {
var other_s = sources.get(i + '');
if (other_s !== undefined) {
set(other_s, UNINITIALIZED);
} else if (i in target) {
// If the item exists in the original, we need to create a uninitialized source,
// else a later read of the property would result in a source being created with
// the value of the original item at that index.
other_s = source(UNINITIALIZED);
sources.set(i + '', other_s);
}
}
}
// If we haven't yet created a source for this property, we need to ensure
// we do so otherwise if we read it later, then the write won't be tracked and
// the heuristics of effects will be different vs if we had read the proxied
@ -211,14 +234,6 @@ export function proxy(value, parent = null, prev) {
check_ownership(metadata);
}
// variable.length = value -> clear all signals with index >= value
if (is_proxied_array && prop === 'length') {
for (var i = value; i < target.length; i += 1) {
var other_s = sources.get(i + '');
if (other_s !== undefined) set(other_s, UNINITIALIZED);
}
}
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
// Set the new value before updating any signals so that any listeners get the new value
@ -232,14 +247,11 @@ export function proxy(value, parent = null, prev) {
// to ensure that iterating over the array as a result of a metadata update
// will not cause the length to be out of sync.
if (is_proxied_array && typeof prop === 'string') {
var ls = sources.get('length');
if (ls !== undefined) {
var n = Number(prop);
var ls = /** @type {Source<number>} */ (sources.get('length'));
var n = Number(prop);
if (Number.isInteger(n) && n >= ls.v) {
set(ls, n + 1);
}
if (Number.isInteger(n) && n >= ls.v) {
set(ls, n + 1);
}
}

@ -85,3 +85,49 @@ test('does not re-proxy proxies', () => {
assert.equal(inner.count, 1);
assert.equal(outer.inner.count, 1);
});
test('deletes a property', () => {
const state = proxy({ a: 1, b: 2 } as { a?: number; b?: number; c?: number });
delete state.a;
assert.equal(JSON.stringify(state), '{"b":2}');
delete state.a;
// deleting a non-existent property should succeed
delete state.c;
});
test('handles array.push', () => {
const original = [1, 2, 3];
const state = proxy(original);
state.push(4);
assert.deepEqual(original.length, 3);
assert.deepEqual(original, [1, 2, 3]);
assert.deepEqual(state.length, 4);
assert.deepEqual(state, [1, 2, 3, 4]);
});
test('handles array mutation', () => {
const original = [1, 2, 3];
const state = proxy(original);
state[3] = 4;
assert.deepEqual(original.length, 3);
assert.deepEqual(original, [1, 2, 3]);
assert.deepEqual(state.length, 4);
assert.deepEqual(state, [1, 2, 3, 4]);
});
test('handles array length mutation', () => {
const original = [1, 2, 3];
const state = proxy(original);
state.length = 0;
assert.deepEqual(original.length, 3);
assert.deepEqual(original, [1, 2, 3]);
assert.deepEqual(original[0], 1);
assert.deepEqual(state.length, 0);
assert.deepEqual(state, []);
assert.deepEqual(state[0], undefined);
});

@ -1,4 +1,4 @@
/** @import { Derived, Effect, Source, Value } from '#client' */
/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
import { DEV } from 'esm-env';
import {
current_component_context,
@ -13,7 +13,9 @@ import {
set_signal_status,
untrack,
increment_version,
update_effect
update_effect,
derived_sources,
set_derived_sources
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import {
@ -34,7 +36,6 @@ let inspect_effects = new Set();
* @param {V} v
* @returns {Source<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function source(v) {
return {
f: 0, // TODO ideally we could skip this altogether, but it causes type errors
@ -45,6 +46,14 @@ export function source(v) {
};
}
/**
* @template V
* @param {V} v
*/
export function state(v) {
return push_derived_source(source(v));
}
/**
* @template V
* @param {V} initial_value
@ -64,6 +73,32 @@ export function mutable_source(initial_value) {
return s;
}
/**
* @template V
* @param {V} v
* @returns {Source<V>}
*/
export function mutable_state(v) {
return push_derived_source(mutable_source(v));
}
/**
* @template V
* @param {Source<V>} source
*/
/*#__NO_SIDE_EFFECTS__*/
function push_derived_source(source) {
if (current_reaction !== null && (current_reaction.f & DERIVED) !== 0) {
if (derived_sources === null) {
set_derived_sources([source]);
} else {
derived_sources.push(source);
}
}
return source;
}
/**
* @template V
* @param {Value<V>} source
@ -84,7 +119,14 @@ export function mutate(source, value) {
* @returns {V}
*/
export function set(source, value) {
if (current_reaction !== null && is_runes() && (current_reaction.f & DERIVED) !== 0) {
if (
current_reaction !== null &&
is_runes() &&
(current_reaction.f & DERIVED) !== 0 &&
// If the source was created locally within the current derived, then
// we allow the mutation.
(derived_sources === null || !derived_sources.includes(source))
) {
e.state_unsafe_mutation();
}

@ -80,6 +80,20 @@ export function set_current_effect(effect) {
current_effect = effect;
}
/**
* When sources are created within a derived, we record them so that we can safely allow
* local mutations to these sources without the side-effect error being invoked unnecessarily.
* @type {null | Source[]}
*/
export let derived_sources = null;
/**
* @param {Source[] | null} sources
*/
export function set_derived_sources(sources) {
derived_sources = sources;
}
/**
* The dependencies of the reaction that is currently being executed. In many cases,
* the dependencies are unchanged between runs, and so this will be `null` unless
@ -230,12 +244,14 @@ function handle_error(error, effect, component_context) {
let current_context = component_context;
while (current_context !== null) {
/** @type {string} */
var filename = current_context.function?.[FILENAME];
if (DEV) {
/** @type {string} */
var filename = current_context.function?.[FILENAME];
if (filename) {
const file = filename.split('/').pop();
component_stack.push(file);
if (filename) {
const file = filename.split('/').pop();
component_stack.push(file);
}
}
current_context = current_context.p;
@ -279,12 +295,14 @@ export function update_reaction(reaction) {
var previous_untracked_writes = current_untracked_writes;
var previous_reaction = current_reaction;
var previous_skip_reaction = current_skip_reaction;
var prev_derived_sources = derived_sources;
new_deps = /** @type {null | Value[]} */ (null);
skipped_deps = 0;
current_untracked_writes = null;
current_reaction = (reaction.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
current_skip_reaction = !is_flushing_effect && (reaction.f & UNOWNED) !== 0;
derived_sources = null;
try {
var result = /** @type {Function} */ (0, reaction.fn)();
@ -321,6 +339,7 @@ export function update_reaction(reaction) {
current_untracked_writes = previous_untracked_writes;
current_reaction = previous_reaction;
current_skip_reaction = previous_skip_reaction;
derived_sources = prev_derived_sources;
}
}
@ -698,6 +717,9 @@ export function get(signal) {
// Register the dependency on the current reaction signal.
if (current_reaction !== null) {
if (derived_sources !== null && derived_sources.includes(signal)) {
e.state_unsafe_local_read();
}
var deps = current_reaction.deps;
// If the signal is accessing the same dependencies in the same

@ -102,7 +102,7 @@ export function validate_binding(binding, get_object, get_property, line, column
ran = true;
if (effect.deps === null) {
var location = filename && `${filename}:${line}:${column}`;
var location = `${filename}:${line}:${column}`;
w.binding_property_non_reactive(binding, location);
warned = true;

@ -56,6 +56,9 @@ function clone(value, cloned, path, paths) {
const unwrapped = cloned.get(value);
if (unwrapped !== undefined) return unwrapped;
if (value instanceof Map) return /** @type {Snapshot<T>} */ (new Map(value));
if (value instanceof Set) return /** @type {Snapshot<T>} */ (new Set(value));
if (is_array(value)) {
const copy = /** @type {Snapshot<any>} */ ([]);
cloned.set(value, copy);

@ -17,9 +17,6 @@ import { define_property } from '../internal/shared/utils.js';
*
* @param {ComponentConstructorOptions<Props> & {
* component: ComponentType<SvelteComponent<Props, Events, Slots>> | Component<Props>;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: boolean;
* }} options
* @returns {SvelteComponent<Props, Events, Slots> & Exports}
*/
@ -64,9 +61,6 @@ class Svelte4Component {
/**
* @param {ComponentConstructorOptions & {
* component: any;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: false;
* }} options
*/
constructor(options) {
@ -110,8 +104,8 @@ class Svelte4Component {
recover: options.recover
});
// We don't flush_sync for custom element wrappers
if (!options?.props?.$$host) {
// We don't flush_sync for custom element wrappers or if the user doesn't want it
if (!options?.props?.$$host || options.sync === false) {
flush_sync();
}

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

@ -10,10 +10,10 @@ export default test({
},
test(assert, target) {
assert.equal(target.querySelector('a')?.getAttribute('href'), '/bar');
assert.equal(target.querySelector('link')?.getAttribute('href'), '/bar');
},
errors: [
'The `href` attribute on `<a href="/bar">...</a>` changed its value between server and client renders. The client value, `/foo`, will be ignored in favour of the server value'
'The `href` attribute on `<link href="/bar">` changed its value between server and client renders. The client value, `/foo`, will be ignored in favour of the server value'
]
});

@ -2,4 +2,4 @@
let { browser } = $props();
</script>
<a href={browser ? '/foo': '/bar'}>foo</a>
<link href={browser ? '/foo' : '/bar'} />

@ -12,6 +12,6 @@ export default test({
assert.htmlEqual(target.innerHTML, '<img src="server.jpg" alt="">');
},
errors: [
'The `src` attribute on `...<img src="server.jpg" alt="">` changed its value between server and client renders. The client value, `client.jpg`, will be ignored in favour of the server value'
'The `src` attribute on `<img src="server.jpg" alt="">` changed its value between server and client renders. The client value, `client.jpg`, will be ignored in favour of the server value'
]
});

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
server_props: {
html: '<div></div>'
},
props: {
html: '<div></div>'
},
test(assert, target) {
assert.htmlEqual(target.innerHTML, '<div></div>');
}
});

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
server_props: {
browser: false
},
props: {
browser: true
}
});

@ -0,0 +1,5 @@
<script>
let { browser } = $props();
</script>
<a href={browser ? '/foo' : '/bar'}>foo</a>

@ -30,6 +30,7 @@ const { test, run } = suite<PreprocessTest>(async (config, cwd) => {
expect(result.dependencies).toEqual(config.dependencies || []);
if (fs.existsSync(`${cwd}/expected_map.json`)) {
delete (result.map as any).ignoreList;
const expected_map = JSON.parse(fs.readFileSync(`${cwd}/expected_map.json`, 'utf-8'));
// You can use https://sokra.github.io/source-map-visualization/#custom to visualize the source map
expect(JSON.parse(JSON.stringify(result.map))).toEqual(expected_map);

@ -6,6 +6,6 @@ export default test({
const p = target.querySelector('p');
ok(p);
assert.equal(window.getComputedStyle(p).color, 'red');
assert.equal(window.getComputedStyle(p).color, 'rgb(255, 0, 0)');
}
});

@ -5,9 +5,9 @@ export default test({
const [control, test] = target.querySelectorAll('p');
assert.equal(window.getComputedStyle(control).color, '');
assert.equal(window.getComputedStyle(control).backgroundColor, '');
assert.equal(window.getComputedStyle(control).backgroundColor, 'rgba(0, 0, 0, 0)');
assert.equal(window.getComputedStyle(test).color, 'red');
assert.equal(window.getComputedStyle(test).backgroundColor, 'black');
assert.equal(window.getComputedStyle(test).color, 'rgb(255, 0, 0)');
assert.equal(window.getComputedStyle(test).backgroundColor, 'rgb(0, 0, 0)');
}
});

@ -9,7 +9,7 @@ export default test({
const p = target.querySelector('p');
ok(p);
const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red');
assert.equal(styles.color, 'rgb(255, 0, 0)');
assert.equal(styles.height, '40px');
}
});

@ -10,7 +10,7 @@ export default test({
ok(p);
const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red');
assert.equal(styles.color, 'rgb(255, 0, 0)');
assert.equal(styles.height, '40px');
}
});

@ -10,7 +10,7 @@ export default test({
ok(p);
let styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red');
assert.equal(styles.color, 'rgb(255, 0, 0)');
component.myColor = 'pink';
component.width = '100vh';
@ -22,7 +22,7 @@ export default test({
target.innerHTML,
'<p style="color: pink; width: 100vh; font-weight: 100; position: absolute;"></p>'
);
assert.equal(styles.color, 'pink');
assert.equal(styles.color, 'rgb(255, 192, 203)');
assert.equal(styles.width, '100vh');
assert.equal(styles.fontWeight, '100');
assert.equal(styles.position, 'absolute');

@ -11,8 +11,8 @@ export default test({
test({ assert, target, window }) {
const [p1, p2] = target.querySelectorAll('p');
assert.equal(window.getComputedStyle(p1).color, 'red');
assert.equal(window.getComputedStyle(p2).color, 'red');
assert.equal(window.getComputedStyle(p1).color, 'rgb(255, 0, 0)');
assert.equal(window.getComputedStyle(p2).color, 'rgb(255, 0, 0)');
const btn = target.querySelector('button');
btn?.click();
@ -27,7 +27,7 @@ export default test({
`
);
assert.equal(window.getComputedStyle(p1).color, 'green');
assert.equal(window.getComputedStyle(p2).color, 'green');
assert.equal(window.getComputedStyle(p1).color, 'rgb(0, 128, 0)');
assert.equal(window.getComputedStyle(p2).color, 'rgb(0, 128, 0)');
}
});

@ -9,8 +9,8 @@ export default test({
test({ assert, component, target, window }) {
const [p1, p2] = target.querySelectorAll('p');
assert.equal(window.getComputedStyle(p1).color, 'red');
assert.equal(window.getComputedStyle(p2).color, 'red');
assert.equal(window.getComputedStyle(p1).color, 'rgb(255, 0, 0)');
assert.equal(window.getComputedStyle(p2).color, 'rgb(255, 0, 0)');
component.color = 'blue';
assert.htmlEqual(
@ -21,7 +21,7 @@ export default test({
`
);
assert.equal(window.getComputedStyle(p1).color, 'blue');
assert.equal(window.getComputedStyle(p2).color, 'blue');
assert.equal(window.getComputedStyle(p1).color, 'rgb(0, 0, 255)');
assert.equal(window.getComputedStyle(p2).color, 'rgb(0, 0, 255)');
}
});

@ -10,7 +10,7 @@ export default test({
ok(p);
let styles = window.getComputedStyle(p);
assert.equal(styles.color, 'green');
assert.equal(styles.color, 'rgb(0, 128, 0)');
component.color = null;
assert.htmlEqual(target.innerHTML, '<p style=""></p>');
@ -29,12 +29,12 @@ export default test({
assert.htmlEqual(target.innerHTML, '<p style="background-color: green;"></p>');
styles = window.getComputedStyle(p);
assert.equal(styles.color, '');
assert.equal(styles.backgroundColor, 'green');
assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
component.color = 'purple';
assert.htmlEqual(target.innerHTML, '<p style="background-color: green; color: purple;"></p>');
styles = window.getComputedStyle(p);
assert.equal(styles.color, 'purple');
assert.equal(styles.backgroundColor, 'green');
assert.equal(styles.color, 'rgb(128, 0, 128)');
assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
}
});

@ -10,7 +10,7 @@ export default test({
ok(p);
const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'blue');
assert.equal(styles.color, 'rgb(0, 0, 255)');
assert.equal(styles.width, '65px');
assert.equal(p.id, 'my-id');

@ -10,7 +10,7 @@ export default test({
ok(p);
const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'blue');
assert.equal(styles.color, 'rgb(0, 0, 255)');
assert.equal(styles.width, '65px');
assert.equal(p.id, 'my-id');
}

@ -10,7 +10,7 @@ export default test({
ok(p);
const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'green');
assert.equal(styles.color, 'rgb(0, 128, 0)');
assert.equal(styles.transform, 'translateX(45px)');
assert.equal(styles.border, '100px solid pink');

@ -10,6 +10,6 @@ export default test({
ok(p);
const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red');
assert.equal(styles.color, 'rgb(255, 0, 0)');
}
});

@ -9,7 +9,7 @@ export default test({
const p = target.querySelector('p');
ok(p);
const styles = window.getComputedStyle(p);
assert.equal(styles.backgroundColor, 'green');
assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
assert.equal(styles.fontSize, '12px');
{
@ -17,7 +17,7 @@ export default test({
const p = target.querySelector('p');
ok(p);
const styles = window.getComputedStyle(p);
assert.equal(styles.backgroundColor, 'green');
assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
assert.equal(styles.fontSize, '50px');
}
}

@ -12,6 +12,6 @@ export default test({
ok(p);
const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red');
assert.equal(styles.color, 'rgb(255, 0, 0)');
}
});

@ -10,12 +10,12 @@ export default test({
ok(p);
let styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red');
assert.equal(styles.color, 'rgb(255, 0, 0)');
assert.equal(styles.fontSize, '20px');
component.color = 'green';
styles = window.getComputedStyle(p);
assert.equal(styles.color, 'green');
assert.equal(styles.color, 'rgb(0, 128, 0)');
}
});

@ -11,7 +11,7 @@ export default test({
let styles = window.getComputedStyle(p);
assert.equal(styles.opacity, '0.5');
assert.equal(styles.color, 'red');
assert.equal(styles.color, 'rgb(255, 0, 0)');
component.styles = 'font-size: 20px';

@ -10,6 +10,6 @@ export default test({
ok(div);
const styles = window.getComputedStyle(div);
assert.equal(styles.color, 'red');
assert.equal(styles.color, 'rgb(255, 0, 0)');
}
});

@ -351,7 +351,6 @@ async function run_test_variant(
component: mod.default,
props: config.props,
target,
immutable: config.immutable,
intro: config.intro,
recover: config.recover ?? false,
hydrate: variant === 'hydrate'

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>10</button>`,
test({ assert, target, logs }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>11</button>`);
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>12</button>`);
assert.deepEqual(logs, [0, 10, 11, 12]);
}
});

@ -0,0 +1,16 @@
<script>
class Counter {
count = $state(0);
constructor(initial) {
$effect.pre(() => {
console.log(this.count);
});
this.count = initial;
}
}
const counter = $derived(new Counter(10));
counter;
</script>
<button onclick={() => counter.count++}>{counter.count}</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>10</button>`,
test({ assert, target, logs }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>11</button>`);
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>12</button>`);
assert.deepEqual(logs, [0, 10, 11, 12]);
}
});

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

Loading…
Cancel
Save