Merge branch 'main' into bindable-types

pull/11225/head
Simon Holthausen 2 years ago
commit 4feb1127cc

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: throw error when auto-subscribed store variable shadow by local variable

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: use keys for hmr modules

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: further improvements to hmr component key generation

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make ownership validation work correctly with HMR

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: remove unstate(), replace with $state.snapshot rune

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix missing classes after dynamic expressions in class attribute

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: loosen proxy signal creation heuristics

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: simplify HMR implementation

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: revert ownership widening change

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: add validation around disallowed sequence expressions to element attributes

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure top level snippets are defined when binding to component prop

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ssr comments in head elements that require raw content

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: use correct meta property for hmr key

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve spreading of attributes

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: hot module reloading support for Svelte 5

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: return ast from `compile` (like Svelte 4 does)

@ -11,6 +11,7 @@
"changesets": [
"afraid-dogs-matter",
"afraid-geckos-dance",
"afraid-kids-hide",
"afraid-moose-matter",
"angry-books-jam",
"angry-plums-punch",
@ -26,16 +27,19 @@
"blue-ants-raise",
"blue-rules-juggle",
"blue-timers-film",
"brave-gorillas-end",
"brave-points-sleep",
"brave-shrimps-kiss",
"brave-walls-destroy",
"brave-walls-flow",
"breezy-carrots-flash",
"breezy-rules-beg",
"bright-peas-juggle",
"bright-snakes-sing",
"brown-houses-obey",
"brown-months-fry",
"brown-spoons-boil",
"calm-pandas-vanish",
"calm-ravens-sneeze",
"chatty-beans-divide",
"chatty-cups-drop",
@ -43,17 +47,21 @@
"chatty-taxis-juggle",
"chilled-pumas-invite",
"chilly-dolphins-lick",
"chilly-rocks-hug",
"chilly-snakes-scream",
"clean-eels-beg",
"clever-chefs-relate",
"clever-rockets-burn",
"clever-sloths-push",
"cold-birds-own",
"cold-masks-learn",
"cool-ants-leave",
"cool-comics-marry",
"cool-peas-lick",
"cool-rabbits-tickle",
"cool-roses-trade",
"cuddly-pianos-drop",
"cuddly-points-tickle",
"curly-lizards-dream",
"curvy-buses-laugh",
"curvy-cups-cough",
@ -74,6 +82,8 @@
"dull-pots-add",
"dull-roses-relate",
"early-ads-tie",
"early-months-whisper",
"eight-hornets-punch",
"eight-steaks-shout",
"eighty-bikes-camp",
"eighty-days-cheat",
@ -83,6 +93,7 @@
"empty-bulldogs-exercise",
"empty-crabs-think",
"empty-tools-share",
"fair-bags-smoke",
"fair-crabs-check",
"fair-pianos-talk",
"fair-spies-repeat",
@ -92,6 +103,7 @@
"fast-weeks-clean",
"few-clouds-shop",
"few-mugs-fail",
"few-teachers-know",
"fifty-rice-wait",
"fifty-steaks-float",
"five-tigers-search",
@ -104,6 +116,8 @@
"forty-peaches-unite",
"forty-suns-smile",
"four-flies-hammer",
"four-mice-hammer",
"fresh-dots-destroy",
"fresh-impalas-bow",
"fresh-weeks-trade",
"friendly-candles-relate",
@ -116,7 +130,9 @@
"gentle-spies-happen",
"giant-moons-own",
"giant-planets-shake",
"giant-plants-grin",
"giant-roses-press",
"gold-tools-nail",
"good-buses-reply",
"good-cars-visit",
"good-pianos-jump",
@ -153,6 +169,7 @@
"khaki-ligers-sing",
"khaki-mails-draw",
"khaki-moose-arrive",
"khaki-tomatoes-rule",
"kind-baboons-approve",
"kind-deers-lay",
"kind-dots-sort",
@ -168,6 +185,7 @@
"lazy-spiders-think",
"lemon-geese-call",
"lemon-geese-drum",
"light-badgers-glow",
"light-days-clean",
"light-humans-hang",
"light-pens-watch",
@ -191,6 +209,7 @@
"metal-lobsters-burn",
"mighty-cooks-scream",
"mighty-files-hammer",
"mighty-frogs-obey",
"moody-carrots-lay",
"moody-frogs-exist",
"moody-houses-argue",
@ -202,6 +221,7 @@
"neat-dingos-clap",
"neat-files-rescue",
"nervous-spoons-relax",
"nervous-turkeys-end",
"new-boats-wait",
"new-brooms-grin",
"new-rabbits-flow",
@ -219,6 +239,7 @@
"old-oranges-compete",
"olive-kangaroos-brake",
"olive-mice-fix",
"olive-moons-act",
"olive-seals-sell",
"olive-shirts-complain",
"olive-socks-kick",
@ -234,7 +255,9 @@
"poor-seahorses-flash",
"popular-ligers-perform",
"popular-mangos-rest",
"popular-walls-hunt",
"pretty-ties-help",
"proud-queens-sniff",
"purple-dragons-peel",
"quiet-apricots-dream",
"quiet-berries-end",
@ -263,6 +286,7 @@
"rotten-rules-invite",
"rude-ghosts-tickle",
"selfish-dragons-knock",
"selfish-socks-smile",
"selfish-spies-help",
"selfish-tools-hide",
"serious-gorillas-eat",
@ -284,6 +308,7 @@
"shiny-shrimps-march",
"short-buses-camp",
"short-countries-rush",
"shy-fishes-drive",
"silent-apes-report",
"silly-laws-happen",
"silly-lies-film",
@ -302,6 +327,7 @@
"small-sheep-type",
"small-spiders-fail",
"smart-cherries-leave",
"smart-grapes-mate",
"smart-parents-swim",
"smart-turkeys-tell",
"smart-zebras-pay",
@ -371,6 +397,7 @@
"three-rice-tie",
"three-suits-grin",
"tidy-buses-whisper",
"tidy-chefs-taste",
"tidy-starfishes-allow",
"tiny-kings-whisper",
"tough-radios-punch",
@ -389,6 +416,7 @@
"wicked-clouds-exercise",
"wicked-doors-train",
"wicked-hairs-cheer",
"wicked-wasps-allow",
"wild-foxes-wonder",
"wild-moose-compare",
"wise-apples-care",
@ -402,6 +430,7 @@
"witty-steaks-dream",
"witty-tomatoes-care",
"witty-years-crash",
"yellow-taxis-double"
"yellow-taxis-double",
"young-masks-refuse"
]
}

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: more accurate default value handling

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: fix string name of reactive map and set iterator

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: generate correct types for reactive Map/Set/Date

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: reactive `URL` and `URLSearchParams` classes

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: update error message for snippet binding and assignments

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: addresses reactive Set bug in certain engines

@ -11,5 +11,5 @@ trim_trailing_whitespace = true
[test/**/expected.css]
insert_final_newline = false
[{package.json,.travis.yml,.eslintrc.json}]
[package.json]
indent_style = space

@ -1,24 +0,0 @@
# NOTE: In general this should be kept in sync with .eslintignore
**/dist/**
**/config/**
**/build/**
**/playgrounds/sandbox/**
**/npm/**
**/*.js.flow
**/*.d.ts
**/playwright*/**
**/vite.config.js
**/vite.prod.config.js
**/node_modules
**/compiler/index.js
**/tests/**
# documentation can contain invalid examples
documentation/**
# contains a fork of the REPL which doesn't adhere to eslint rules
sites/svelte-5-preview/**
# Wasn't checked previously, reenable at some point
sites/svelte.dev/**

@ -1,54 +0,0 @@
module.exports = {
extends: ['@sveltejs'],
// TODO: add runes to eslint-plugin-svelte
globals: {
$state: true,
$derived: true,
$effect: true,
$props: true
},
overrides: [
{
// scripts and playground should be console logging so don't lint against them
files: ['playgrounds/**/*', 'scripts/**/*'],
rules: {
'no-console': 'off'
}
},
{
// the playgrounds can use public naming conventions since they're examples
files: ['playgrounds/**/*'],
rules: {
'lube/svelte-naming-convention': 'off'
}
},
{
files: ['packages/svelte/src/compiler/**/*'],
rules: {
'no-var': 'error'
}
}
],
plugins: ['lube'],
rules: {
'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
// eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off
'object-shorthand': 'off',
'no-var': 'off',
// TODO: enable these rules and run `pnpm lint:fix`
// skipping that for now so as to avoid impacting real work
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'prefer-const': 'off',
'svelte/valid-compile': 'off',
quotes: 'off'
}
};

@ -1,5 +1,3 @@
# NOTE: In general this should be kept in sync with .eslintignore
packages/**/dist/*.js
packages/**/build/*.js
packages/**/npm/**/*
@ -31,7 +29,6 @@ sites/svelte.dev/src/lib/generated
**/.vercel
.github/CODEOWNERS
.prettierignore
.eslintignore
.changeset
pnpm-lock.yaml
pnpm-workspace.yaml

@ -0,0 +1,45 @@
import svelte_config from '@sveltejs/eslint-config';
import lube from 'eslint-plugin-lube';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
...svelte_config,
{
plugins: {
lube
},
rules: {
'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
// eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off
'object-shorthand': 'off',
'no-var': 'off',
// TODO: enable these rules and run `pnpm lint:fix`
// skipping that for now so as to avoid impacting real work
'@stylistic/quotes': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'prefer-const': 'off'
}
},
{
files: ['playgrounds/**/*'],
rules: {
'lube/svelte-naming-convention': 'off',
'no-console': 'off'
}
},
{
ignores: [
'**/*.d.ts',
'**/tests',
'packages/svelte/compiler/index.js',
// documentation can contain invalid examples
'documentation',
// contains a fork of the REPL which doesn't adhere to eslint rules
'sites/svelte-5-preview/**',
// wasn't checked previously, reenable at some point
'sites/svelte.dev/**'
]
}
];

@ -19,28 +19,28 @@
"preview-site": "npm run build --prefix sites/svelte-5-preview",
"check": "cd packages/svelte && pnpm build && cd ../../ && pnpm -r check",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ./",
"test": "vitest run --coverage",
"test-output": "vitest run --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
"lint": "prettier --check . && eslint",
"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"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@sveltejs/eslint-config": "^6.0.4",
"@sveltejs/eslint-config": "^7.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@vitest/coverage-v8": "^1.2.1",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"eslint": "^9.0.0",
"eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0",
"playwright": "^1.41.1",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"typescript": "^5.3.3",
"typescript-eslint": "^7.6.0",
"vitest": "^1.2.1"
},
"pnpm": {

@ -1,5 +1,103 @@
# svelte
## 5.0.0-next.105
### Patch Changes
- breaking: remove unstate(), replace with $state.snapshot rune ([#11180](https://github.com/sveltejs/svelte/pull/11180))
- fix: more accurate default value handling ([#11183](https://github.com/sveltejs/svelte/pull/11183))
## 5.0.0-next.104
### Patch Changes
- fix: ssr comments in head elements that require raw content ([#10936](https://github.com/sveltejs/svelte/pull/10936))
- fix: improve spreading of attributes ([#11177](https://github.com/sveltejs/svelte/pull/11177))
## 5.0.0-next.103
### Patch Changes
- fix: throw error when auto-subscribed store variable shadow by local variable ([#11170](https://github.com/sveltejs/svelte/pull/11170))
- fix: make ownership validation work correctly with HMR ([#11171](https://github.com/sveltejs/svelte/pull/11171))
- fix: revert ownership widening change ([#11161](https://github.com/sveltejs/svelte/pull/11161))
- fix: fix string name of reactive map and set iterator ([#11169](https://github.com/sveltejs/svelte/pull/11169))
- feat: reactive `URL` and `URLSearchParams` classes ([#11157](https://github.com/sveltejs/svelte/pull/11157))
- feat: update error message for snippet binding and assignments ([#11168](https://github.com/sveltejs/svelte/pull/11168))
## 5.0.0-next.102
### Patch Changes
- fix: generate correct types for reactive Map/Set/Date ([#11153](https://github.com/sveltejs/svelte/pull/11153))
## 5.0.0-next.101
### Patch Changes
- fix missing classes after dynamic expressions in class attribute ([#11134](https://github.com/sveltejs/svelte/pull/11134))
- feat: simplify HMR implementation ([#11132](https://github.com/sveltejs/svelte/pull/11132))
- fix: add validation around disallowed sequence expressions to element attributes ([#11149](https://github.com/sveltejs/svelte/pull/11149))
## 5.0.0-next.100
### Patch Changes
- fix: further improvements to hmr component key generation ([#11129](https://github.com/sveltejs/svelte/pull/11129))
## 5.0.0-next.99
### Patch Changes
- fix: use correct meta property for hmr key ([#11125](https://github.com/sveltejs/svelte/pull/11125))
## 5.0.0-next.98
### Patch Changes
- fix: use keys for hmr modules ([#11123](https://github.com/sveltejs/svelte/pull/11123))
- fix: addresses reactive Set bug in certain engines ([#11120](https://github.com/sveltejs/svelte/pull/11120))
## 5.0.0-next.97
### Patch Changes
- fix: loosen proxy signal creation heuristics ([#11109](https://github.com/sveltejs/svelte/pull/11109))
- fix: ensure top level snippets are defined when binding to component prop ([#11104](https://github.com/sveltejs/svelte/pull/11104))
- feat: hot module reloading support for Svelte 5 ([#11106](https://github.com/sveltejs/svelte/pull/11106))
## 5.0.0-next.96
### Patch Changes
- feat: introduce `$host` rune, deprecate `createEventDispatcher` ([#11059](https://github.com/sveltejs/svelte/pull/11059))
- fix: execute sole static script tag ([#11095](https://github.com/sveltejs/svelte/pull/11095))
- fix: make static `element` property available for the SvelteComponent type ([#11079](https://github.com/sveltejs/svelte/pull/11079))
- fix: improve internal proxied state signal heuristic ([#11102](https://github.com/sveltejs/svelte/pull/11102))
- fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags ([#11096](https://github.com/sveltejs/svelte/pull/11096))
- fix: ensure deep mutation ownership widening ([#11094](https://github.com/sveltejs/svelte/pull/11094))
- fix: improve compiled output of multiple call expression in single text node ([#11097](https://github.com/sveltejs/svelte/pull/11097))
- fix: improve hydration of svelte head blocks ([#11099](https://github.com/sveltejs/svelte/pull/11099))
## 5.0.0-next.95
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.95",
"version": "5.0.0-next.105",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -104,7 +104,7 @@
"check": "tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
"prepublishOnly": "pnpm build",
"knip": "knip"
},
@ -116,7 +116,7 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"dts-buddy": "^0.4.6",
"dts-buddy": "^0.4.7",
"esbuild": "^0.19.11",
"knip": "^4.2.1",
"rollup": "^4.9.5",

@ -42,6 +42,26 @@ declare namespace $state {
*/
export function frozen<T>(initial: T): Readonly<T>;
export function frozen<T>(): Readonly<T> | undefined;
/**
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
* Example:
* ```ts
* <script>
* let counter = $state({ count: 0 });
*
* function onclick() {
* // Will log `{ count: ... }` rather than `Proxy { ... }`
* console.log($state.snapshot(counter));
* };
* </script>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state.snapshot
*
* @param state The value to snapshot
*/
export function snapshot<T>(state: T): T;
}
/**

@ -214,6 +214,7 @@ const runes = {
'duplicate-props-rune': () => `Cannot use $props() more than once`,
'invalid-each-assignment': () =>
`Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. 'array[i] = value' instead of 'entry = value')`,
'invalid-snippet-assignment': () => `Cannot reassign or bind to snippet parameter`,
'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)`,
'conflicting-property-name': () =>
`Cannot have a property and a component export with the same name`

@ -44,6 +44,7 @@ export function compile(source, options) {
const analysis = analyze_component(parsed, source, combined_options);
const result = transform_component(analysis, source, combined_options);
result.ast = to_public_ast(source, parsed, options.modernAst);
return result;
} catch (e) {
if (e instanceof CompileError) {
@ -147,7 +148,16 @@ export function parse(source, options = {}) {
throw e;
}
if (options.modern) {
return to_public_ast(source, ast, options.modern);
}
/**
* @param {string} source
* @param {import('#compiler').Root} ast
* @param {boolean | undefined} modern
*/
function to_public_ast(source, ast, modern) {
if (modern) {
// remove things that we don't want to treat as public API
return walk(ast, null, {
_(node, { next }) {

@ -141,29 +141,37 @@ export function convert(source, ast) {
};
if (node.pending) {
const first = /** @type {import('#compiler').BaseNode} */ (node.pending.nodes.at(0));
const last = /** @type {import('#compiler').BaseNode} */ (node.pending.nodes.at(-1));
const first = node.pending.nodes.at(0);
const last = node.pending.nodes.at(-1);
pendingblock.start = first.start;
pendingblock.end = last.end;
pendingblock.start = first?.start ?? source.indexOf('}', node.expression.end) + 1;
pendingblock.end = last?.end ?? pendingblock.start;
pendingblock.skip = false;
}
if (node.then) {
const first = /** @type {import('#compiler').BaseNode} */ (node.then.nodes.at(0));
const last = /** @type {import('#compiler').BaseNode} */ (node.then.nodes.at(-1));
const first = node.then.nodes.at(0);
const last = node.then.nodes.at(-1);
thenblock.start = pendingblock.end ?? first.start;
thenblock.end = last.end;
thenblock.start =
pendingblock.end ?? first?.start ?? source.indexOf('}', node.expression.end) + 1;
thenblock.end =
last?.end ?? source.lastIndexOf('}', pendingblock.end ?? node.expression.end) + 1;
thenblock.skip = false;
}
if (node.catch) {
const first = /** @type {import('#compiler').BaseNode} */ (node.catch.nodes.at(0));
const last = /** @type {import('#compiler').BaseNode} */ (node.catch.nodes.at(-1));
catchblock.start = thenblock.end ?? pendingblock.end ?? first.start;
catchblock.end = last.end;
const first = node.catch.nodes.at(0);
const last = node.catch.nodes.at(-1);
catchblock.start =
thenblock.end ??
pendingblock.end ??
first?.start ??
source.indexOf('}', node.expression.end) + 1;
catchblock.end =
last?.end ??
source.lastIndexOf('}', thenblock.end ?? pendingblock.end ?? node.expression.end) + 1;
catchblock.skip = false;
}

@ -302,8 +302,8 @@ export function analyze_component(root, source, options) {
declaration.initial.source.value === 'svelte/store'
))
) {
let is_nested_store_subscription = false;
for (const reference of references) {
let is_nested_store_subscription_node = undefined;
search: for (const reference of references) {
for (let i = reference.path.length - 1; i >= 0; i--) {
const scope =
scopes.get(reference.path[i]) ||
@ -311,14 +311,17 @@ export function analyze_component(root, source, options) {
instance.scopes.get(reference.path[i]);
if (scope) {
const owner = scope?.owner(store_name);
is_nested_store_subscription =
!!owner && owner !== module.scope && owner !== instance.scope;
if (!!owner && owner !== module.scope && owner !== instance.scope) {
is_nested_store_subscription_node = reference.node;
break search;
}
break;
}
}
}
if (is_nested_store_subscription) {
error(references[0].node, 'illegal-store-subscription');
if (is_nested_store_subscription_node) {
error(is_nested_store_subscription_node, 'illegal-store-subscription');
}
if (options.runes !== false) {

@ -98,7 +98,12 @@ function validate_element(node, context) {
if (context.state.analysis.runes && is_expression) {
const expression = attribute.value[0].expression;
if (expression.type === 'SequenceExpression') {
error(expression, 'invalid-sequence-expression');
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') error(expression, 'invalid-sequence-expression');
}
}
}
@ -348,7 +353,9 @@ const validation = {
error(node, 'invalid-each-assignment');
}
// TODO handle mutations of non-state/props in runes mode
if (binding.kind === 'snippet') {
error(node, 'invalid-snippet-assignment');
}
}
if (node.name === 'group') {
@ -858,6 +865,12 @@ function validate_call_expression(node, scope, path) {
error(node, 'invalid-rune-args-length', rune, [1]);
}
}
if (rune === '$state.snapshot') {
if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
}
}
}
/**
@ -1008,14 +1021,21 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
function validate_assignment(node, argument, state) {
validate_no_const_assignment(node, argument, state.scope, false);
if (state.analysis.runes && argument.type === 'Identifier') {
if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name);
if (binding?.kind === 'derived') {
error(node, 'invalid-derived-assignment');
if (state.analysis.runes) {
if (binding?.kind === 'derived') {
error(node, 'invalid-derived-assignment');
}
if (binding?.kind === 'each') {
error(node, 'invalid-each-assignment');
}
}
if (binding?.kind === 'each') {
error(node, 'invalid-each-assignment');
if (binding?.kind === 'snippet') {
error(node, 'invalid-snippet-assignment');
}
}

@ -415,15 +415,38 @@ export function client_component(source, analysis, options) {
const body = [
...state.hoisted,
...module.body,
b.export_default(
b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
component_block
)
b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
component_block
)
];
if (options.hmr) {
body.push(
b.if(
b.id('import.meta.hot'),
b.block([
b.const(b.id('s'), b.call('$.source', b.id(analysis.name))),
b.stmt(b.assignment('=', b.id(analysis.name), b.call('$.hmr', b.id('s')))),
b.stmt(
b.call(
'import.meta.hot.accept',
b.arrow(
[b.id('module')],
b.block([
b.stmt(b.call('$.set', b.id('s'), b.member(b.id('module'), b.id('default'))))
])
)
)
)
])
)
);
}
body.push(b.export_default(b.id(analysis.name)));
if (options.dev) {
if (options.filename) {
let filename = options.filename;
@ -441,8 +464,8 @@ export function client_component(source, analysis, options) {
);
}
body.unshift(b.stmt(b.call(b.id('$.mark_module_start'), b.id(analysis.name))));
body.push(b.stmt(b.call(b.id('$.mark_module_end'))));
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))));
}
if (options.discloseVersion) {

@ -1,5 +1,10 @@
import * as b from '../../../utils/builders.js';
import { extract_paths, is_simple_expression, object } from '../../../utils/ast.js';
import {
extract_paths,
is_expression_async,
is_simple_expression,
object
} from '../../../utils/ast.js';
import { error } from '../../../errors.js';
import {
PROPS_IS_LAZY_INITIAL,
@ -115,103 +120,6 @@ export function serialize_get_binding(node, state) {
return node;
}
/**
* @param {import('estree').Expression | import('estree').Pattern} expression
* @returns {boolean}
*/
function is_expression_async(expression) {
switch (expression.type) {
case 'AwaitExpression': {
return true;
}
case 'ArrayPattern': {
return expression.elements.some((element) => element && is_expression_async(element));
}
case 'ArrayExpression': {
return expression.elements.some((element) => {
if (!element) {
return false;
} else if (element.type === 'SpreadElement') {
return is_expression_async(element.argument);
} else {
return is_expression_async(element);
}
});
}
case 'AssignmentPattern':
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression': {
return is_expression_async(expression.left) || is_expression_async(expression.right);
}
case 'CallExpression':
case 'NewExpression': {
return (
(expression.callee.type !== 'Super' && is_expression_async(expression.callee)) ||
expression.arguments.some((element) => {
if (element.type === 'SpreadElement') {
return is_expression_async(element.argument);
} else {
return is_expression_async(element);
}
})
);
}
case 'ChainExpression': {
return is_expression_async(expression.expression);
}
case 'ConditionalExpression': {
return (
is_expression_async(expression.test) ||
is_expression_async(expression.alternate) ||
is_expression_async(expression.consequent)
);
}
case 'ImportExpression': {
return is_expression_async(expression.source);
}
case 'MemberExpression': {
return (
(expression.object.type !== 'Super' && is_expression_async(expression.object)) ||
(expression.property.type !== 'PrivateIdentifier' &&
is_expression_async(expression.property))
);
}
case 'ObjectPattern':
case 'ObjectExpression': {
return expression.properties.some((property) => {
if (property.type === 'SpreadElement') {
return is_expression_async(property.argument);
} else if (property.type === 'Property') {
return (
(property.key.type !== 'PrivateIdentifier' && is_expression_async(property.key)) ||
is_expression_async(property.value)
);
}
});
}
case 'RestElement': {
return is_expression_async(expression.argument);
}
case 'SequenceExpression':
case 'TemplateLiteral': {
return expression.expressions.some((subexpression) => is_expression_async(subexpression));
}
case 'TaggedTemplateExpression': {
return is_expression_async(expression.tag) || is_expression_async(expression.quasi);
}
case 'UnaryExpression':
case 'UpdateExpression': {
return is_expression_async(expression.argument);
}
case 'YieldExpression': {
return expression.argument ? is_expression_async(expression.argument) : false;
}
default:
return false;
}
}
/**
* @template {import('./types').ClientTransformState} State
* @param {import('estree').AssignmentExpression} node

@ -388,6 +388,13 @@ export const javascript_visitors_runes = {
return b.call('$.effect_active');
}
if (rune === '$state.snapshot') {
return b.call(
'$.snapshot',
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
);
}
if (rune === '$effect.root') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => context.visit(arg))

@ -2277,27 +2277,24 @@ export const template_visitors = {
for (const path of paths) {
const name = /** @type {import('estree').Identifier} */ (path.node).name;
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(name));
const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk(
/** @type {import('estree').Expression} */ (context.visit(path.expression?.(unwrapped)))
);
declarations.push(
b.let(
path.node,
b.thunk(
/** @type {import('estree').Expression} */ (
context.visit(path.expression?.(unwrapped))
)
)
)
b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)
);
binding.expression = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
binding.mutation = create_mutation(
/** @type {import('estree').Pattern} */ (path.update_expression(unwrapped))
);
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (context.state.options.dev) {
declarations.push(b.stmt(b.call(name)));
declarations.push(b.stmt(binding.expression));
}
binding.expression = b.call(name);
binding.mutation = create_mutation(
/** @type {import('estree').Pattern} */ (path.update_expression(unwrapped))
);
}
}
@ -2315,7 +2312,7 @@ export const template_visitors = {
)
: /** @type {import('estree').Expression} */ (context.visit(node.key))
)
: b.literal(null);
: b.id('$.index');
if (node.index && each_node_meta.contains_group_binding) {
// We needed to create a unique identifier for the index above, but we want to use the
@ -2323,36 +2320,21 @@ export const template_visitors = {
declarations.push(b.let(node.index, index));
}
let callee = '$.each_indexed';
/** @type {import('estree').Expression[]} */
const args = [];
if ((each_type & EACH_KEYED) !== 0) {
if (context.state.options.dev && key_function.type !== 'Literal') {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
callee = '$.each_keyed';
args.push(
context.state.node,
b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children)))
);
} else {
args.push(
context.state.node,
b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children)))
if (context.state.options.dev && (each_type & EACH_KEYED) !== 0) {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
/** @type {import('estree').Expression[]} */
const args = [
context.state.node,
b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children)))
];
if (node.fallback) {
args.push(
b.arrow(
@ -2362,7 +2344,7 @@ export const template_visitors = {
);
}
context.state.init.push(b.stmt(b.call(callee, ...args)));
context.state.init.push(b.stmt(b.call('$.each', ...args)));
},
IfBlock(node, context) {
context.state.template.push('<!>');
@ -2501,24 +2483,23 @@ export const template_visitors = {
for (const path of paths) {
const name = /** @type {import('estree').Identifier} */ (path.node).name;
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(name));
declarations.push(
b.let(
path.node,
b.thunk(
/** @type {import('estree').Expression} */ (
context.visit(path.expression?.(b.maybe_call(b.id(arg_alias))))
)
)
const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk(
/** @type {import('estree').Expression} */ (
context.visit(path.expression?.(b.maybe_call(b.id(arg_alias))))
)
);
declarations.push(
b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)
);
binding.expression = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (context.state.options.dev) {
declarations.push(b.stmt(b.call(name)));
declarations.push(b.stmt(binding.expression));
}
binding.expression = b.call(name);
}
}

@ -20,7 +20,8 @@ export function transform_component(analysis, source, options) {
warnings: transform_warnings(source, options.filename, analysis.warnings),
metadata: {
runes: analysis.runes
}
},
ast: /** @type {any} */ (null) // set afterwards
};
}
@ -62,7 +63,8 @@ export function transform_component(analysis, source, options) {
warnings: transform_warnings(source, options.filename, analysis.warnings), // TODO apply preprocessor sourcemap
metadata: {
runes: analysis.runes
}
},
ast: /** @type {any} */ (null) // set afterwards
};
}
@ -80,7 +82,8 @@ export function transform_module(analysis, source, options) {
warnings: transform_warnings(source, analysis.name, analysis.warnings),
metadata: {
runes: true
}
},
ast: /** @type {any} */ (null) // set afterwards
};
}
@ -105,7 +108,8 @@ export function transform_module(analysis, source, options) {
warnings: transform_warnings(source, analysis.name, analysis.warnings),
metadata: {
runes: true
}
},
ast: /** @type {any} */ (null) // set afterwards
};
}

@ -4,6 +4,7 @@ import {
extract_identifiers,
extract_paths,
is_event_attribute,
is_expression_async,
unwrap_optional
} from '../../../utils/ast.js';
import * as b from '../../../utils/builders.js';
@ -793,6 +794,10 @@ const javascript_visitors_runes = {
return b.literal(false);
}
if (rune === '$state.snapshot') {
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
}
if (rune === '$inspect' || rune === '$inspect().with') {
return transform_inspect_rune(node, context);
}
@ -853,9 +858,7 @@ function serialize_attribute_value(
/** @type {import('estree').Expression[]} */
const expressions = [];
if (attribute_value[0].type !== 'Text') {
quasis.push(b.quasi('', false));
}
quasis.push(b.quasi('', false));
let i = 0;
for (const node of attribute_value) {
@ -866,7 +869,8 @@ function serialize_attribute_value(
// don't trim, space could be important to separate from expression tag
data = data.replace(regex_whitespaces_strict, ' ');
}
quasis.push(b.quasi(data, i === attribute_value.length));
const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += data;
} else {
expressions.push(
b.call(
@ -874,9 +878,7 @@ function serialize_attribute_value(
/** @type {import('estree').Expression} */ (context.visit(node.expression))
)
);
if (i === attribute_value.length || attribute_value[i]?.type !== 'Text') {
quasis.push(b.quasi('', true));
}
quasis.push(b.quasi('', i + 1 === attribute_value.length));
}
}
@ -1194,7 +1196,9 @@ const javascript_visitors_legacy = {
const name = /** @type {import('estree').Identifier} */ (path.node).name;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true);
declarations.push(b.declarator(path.node, b.call('$.value_or_fallback', prop, value)));
declarations.push(
b.declarator(path.node, b.call('$.value_or_fallback', prop, b.thunk(value)))
);
}
continue;
}
@ -1207,13 +1211,15 @@ const javascript_visitors_legacy = {
b.literal(binding.prop_alias ?? declarator.id.name),
true
);
const init = declarator.init
? b.call(
'$.value_or_fallback',
prop,
/** @type {import('estree').Expression} */ (visit(declarator.init))
)
: prop;
/** @type {import('estree').Expression} */
let init = prop;
if (declarator.init) {
const default_value = /** @type {import('estree').Expression} */ (visit(declarator.init));
init = is_expression_async(default_value)
? b.await(b.call('$.value_or_fallback_async', prop, b.thunk(default_value, true)))
: b.call('$.value_or_fallback', prop, b.thunk(default_value));
}
declarations.push(b.declarator(declarator.id, init));
@ -1614,14 +1620,15 @@ const template_visitors = {
state.template.push(block_close);
},
SnippetBlock(node, context) {
// TODO hoist where possible
context.state.init.push(
b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
/** @type {import('estree').BlockStatement} */ (context.visit(node.body))
)
const fn = b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
/** @type {import('estree').BlockStatement} */ (context.visit(node.body))
);
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
// TODO hoist where possible
context.state.init.push(fn);
if (context.state.options.dev) {
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
@ -2221,14 +2228,27 @@ export function server_component(analysis, options) {
// If the component binds to a child, we need to put the template in a loop and repeat until legacy bindings are stable.
// We can remove this once the legacy syntax is gone.
if (analysis.uses_component_bindings) {
const snippets = template.body.filter(
(node) =>
node.type === 'FunctionDeclaration' &&
// @ts-expect-error
node.___snippet
);
const rest = template.body.filter(
(node) =>
node.type !== 'FunctionDeclaration' ||
// @ts-expect-error
!node.___snippet
);
template.body = [
...snippets,
b.let('$$settled', b.true),
b.let('$$inner_payload'),
b.stmt(
b.function(
b.id('$$render_inner'),
[b.id('$$payload')],
b.block(/** @type {import('estree').Statement[]} */ (template.body))
b.block(/** @type {import('estree').Statement[]} */ (rest))
)
),
b.do_while(

@ -31,6 +31,7 @@ export const PassiveEvents = ['wheel', 'touchstart', 'touchmove', 'touchend', 't
export const Runes = /** @type {const} */ ([
'$state',
'$state.frozen',
'$state.snapshot',
'$props',
'$bindable',
'$derived',

@ -649,7 +649,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
for (const param of node.parameters) {
for (const id of extract_identifiers(param)) {
child_scope.declare(id, 'each', 'let');
child_scope.declare(id, 'snippet', 'let');
}
}

@ -46,6 +46,8 @@ export interface CompileResult {
*/
runes: boolean;
};
/** The AST */
ast: any;
}
export interface Warning {
@ -178,6 +180,19 @@ export interface CompileOptions extends ModuleCompileOptions {
* @default null
*/
cssOutputFilename?: string;
/**
* If `true`, compiles components with hot reloading support.
*
* @default false
*/
hmr?: boolean;
/**
* If `true`, returns the modern version of the AST.
* Will become `true` by default in Svelte 6, and the option will be removed in Svelte 7.
*
* @default false
*/
modernAst?: boolean;
}
export interface ModuleCompileOptions {
@ -225,6 +240,7 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
legacy: Required<Required<CompileOptions>['legacy']>;
runes: CompileOptions['runes'];
customElementOptions: SvelteOptions['customElement'];
hmr: CompileOptions['hmr'];
};
export type DeclarationKind =
@ -246,7 +262,8 @@ export interface Binding {
* - `rest_prop`: A rest prop
* - `state`: A state variable
* - `derived`: A derived variable
* - `each`: An each block context variable
* - `each`: An each block parameter
* - `snippet`: A snippet parameter
* - `store_sub`: A $store value
* - `legacy_reactive`: A `$:` declaration
* - `legacy_reactive_import`: An imported binding that is mutated inside the component
@ -260,6 +277,7 @@ export interface Binding {
| 'frozen_state'
| 'derived'
| 'each'
| 'snippet'
| 'store_sub'
| 'legacy_reactive'
| 'legacy_reactive_import';

@ -183,11 +183,11 @@ export function extract_identifiers_from_destructuring(node, nodes = []) {
* @typedef {Object} DestructuredAssignment
* @property {import('estree').Identifier | import('estree').MemberExpression} node The node the destructuring path end in. Can be a member expression only for assignment expressions
* @property {boolean} is_rest `true` if this is a `...rest` destructuring
* @property {(expression: import('estree').Expression) => import('estree').Identifier | import('estree').MemberExpression | import('estree').CallExpression} expression Returns an expression which walks the path starting at the given expression.
* This will be a call expression if a rest element or default is involved
* e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux`
* since we can't represent `baz` or `rest` purely as a path
* @property {(expression: import('estree').Expression) => import('estree').Identifier | import('estree').MemberExpression | import('estree').CallExpression} update_expression Like `expression` but without default values.
* @property {boolean} has_default_value `true` if this has a fallback value like `const { foo = 'bar } = ..`
* @property {(expression: import('estree').Expression) => import('estree').Identifier | import('estree').MemberExpression | import('estree').CallExpression | import('estree').AwaitExpression} expression Returns an expression which walks the path starting at the given expression.
* This will be a call expression if a rest element or default is involved e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux` since we can't represent `baz` or `rest` purely as a path
* Will be an await expression in case of an async default value (`const { foo = await bar } = ...`)
* @property {(expression: import('estree').Expression) => import('estree').Identifier | import('estree').MemberExpression | import('estree').CallExpression | import('estree').AwaitExpression} update_expression Like `expression` but without default values.
*/
/**
@ -200,7 +200,8 @@ export function extract_paths(param) {
[],
param,
(node) => /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (node),
(node) => /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (node)
(node) => /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (node),
false
);
}
@ -209,15 +210,17 @@ export function extract_paths(param) {
* @param {import('estree').Node} param
* @param {DestructuredAssignment['expression']} expression
* @param {DestructuredAssignment['update_expression']} update_expression
* @param {boolean} has_default_value
* @returns {DestructuredAssignment[]}
*/
function _extract_paths(assignments = [], param, expression, update_expression) {
function _extract_paths(assignments = [], param, expression, update_expression, has_default_value) {
switch (param.type) {
case 'Identifier':
case 'MemberExpression':
assignments.push({
node: param,
is_rest: false,
has_default_value,
expression,
update_expression
});
@ -246,17 +249,30 @@ function _extract_paths(assignments = [], param, expression, update_expression)
assignments.push({
node: prop.argument,
is_rest: true,
has_default_value,
expression: rest_expression,
update_expression: rest_expression
});
} else {
_extract_paths(assignments, prop.argument, rest_expression, rest_expression);
_extract_paths(
assignments,
prop.argument,
rest_expression,
rest_expression,
has_default_value
);
}
} else {
/** @type {DestructuredAssignment['expression']} */
const object_expression = (object) =>
b.member(expression(object), prop.key, prop.computed || prop.key.type !== 'Identifier');
_extract_paths(assignments, prop.value, object_expression, object_expression);
_extract_paths(
assignments,
prop.value,
object_expression,
object_expression,
has_default_value
);
}
}
@ -274,16 +290,29 @@ function _extract_paths(assignments = [], param, expression, update_expression)
assignments.push({
node: element.argument,
is_rest: true,
has_default_value,
expression: rest_expression,
update_expression: rest_expression
});
} else {
_extract_paths(assignments, element.argument, rest_expression, rest_expression);
_extract_paths(
assignments,
element.argument,
rest_expression,
rest_expression,
has_default_value
);
}
} else {
/** @type {DestructuredAssignment['expression']} */
const array_expression = (object) => b.member(expression(object), b.literal(i), true);
_extract_paths(assignments, element, array_expression, array_expression);
_extract_paths(
assignments,
element,
array_expression,
array_expression,
has_default_value
);
}
}
}
@ -293,16 +322,22 @@ function _extract_paths(assignments = [], param, expression, update_expression)
case 'AssignmentPattern': {
/** @type {DestructuredAssignment['expression']} */
const fallback_expression = (object) =>
b.call('$.value_or_fallback', expression(object), param.right);
is_expression_async(param.right)
? b.await(
b.call('$.value_or_fallback_async', expression(object), b.thunk(param.right, true))
)
: b.call('$.value_or_fallback', expression(object), b.thunk(param.right));
if (param.left.type === 'Identifier') {
assignments.push({
node: param.left,
is_rest: false,
has_default_value: true,
expression: fallback_expression,
update_expression
});
} else {
_extract_paths(assignments, param.left, fallback_expression, update_expression);
_extract_paths(assignments, param.left, fallback_expression, update_expression, true);
}
break;
@ -370,3 +405,100 @@ export function is_simple_expression(node) {
export function unwrap_optional(node) {
return node.type === 'ChainExpression' ? node.expression : node;
}
/**
* @param {import('estree').Expression | import('estree').Pattern} expression
* @returns {boolean}
*/
export function is_expression_async(expression) {
switch (expression.type) {
case 'AwaitExpression': {
return true;
}
case 'ArrayPattern': {
return expression.elements.some((element) => element && is_expression_async(element));
}
case 'ArrayExpression': {
return expression.elements.some((element) => {
if (!element) {
return false;
} else if (element.type === 'SpreadElement') {
return is_expression_async(element.argument);
} else {
return is_expression_async(element);
}
});
}
case 'AssignmentPattern':
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression': {
return is_expression_async(expression.left) || is_expression_async(expression.right);
}
case 'CallExpression':
case 'NewExpression': {
return (
(expression.callee.type !== 'Super' && is_expression_async(expression.callee)) ||
expression.arguments.some((element) => {
if (element.type === 'SpreadElement') {
return is_expression_async(element.argument);
} else {
return is_expression_async(element);
}
})
);
}
case 'ChainExpression': {
return is_expression_async(expression.expression);
}
case 'ConditionalExpression': {
return (
is_expression_async(expression.test) ||
is_expression_async(expression.alternate) ||
is_expression_async(expression.consequent)
);
}
case 'ImportExpression': {
return is_expression_async(expression.source);
}
case 'MemberExpression': {
return (
(expression.object.type !== 'Super' && is_expression_async(expression.object)) ||
(expression.property.type !== 'PrivateIdentifier' &&
is_expression_async(expression.property))
);
}
case 'ObjectPattern':
case 'ObjectExpression': {
return expression.properties.some((property) => {
if (property.type === 'SpreadElement') {
return is_expression_async(property.argument);
} else if (property.type === 'Property') {
return (
(property.key.type !== 'PrivateIdentifier' && is_expression_async(property.key)) ||
is_expression_async(property.value)
);
}
});
}
case 'RestElement': {
return is_expression_async(expression.argument);
}
case 'SequenceExpression':
case 'TemplateLiteral': {
return expression.expressions.some((subexpression) => is_expression_async(subexpression));
}
case 'TaggedTemplateExpression': {
return is_expression_async(expression.tag) || is_expression_async(expression.quasi);
}
case 'UnaryExpression':
case 'UpdateExpression': {
return is_expression_async(expression.argument);
}
case 'YieldExpression': {
return expression.argument ? is_expression_async(expression.argument) : false;
}
default:
return false;
}
}

@ -398,9 +398,10 @@ export function template(elements, expressions) {
/**
* @param {import('estree').Expression | import('estree').BlockStatement} expression
* @param {boolean} [async]
* @returns {import('estree').Expression}
*/
export function thunk(expression) {
export function thunk(expression, async = false) {
if (
expression.type === 'CallExpression' &&
expression.callee.type !== 'Super' &&
@ -411,7 +412,9 @@ export function thunk(expression) {
return expression.callee;
}
return arrow([], expression);
const fn = arrow([], expression);
if (async) fn.async = true;
return fn;
}
/**

@ -95,6 +95,8 @@ export const validate_component_options =
runes: boolean(undefined),
hmr: boolean(false),
sourcemap: validator(undefined, (input) => {
// Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map,
// so there's no good way to check type validity here

@ -25,6 +25,9 @@ export const HYDRATION_END_ELSE = `${HYDRATION_END}!`; // used to indicate that
export const UNINITIALIZED = Symbol();
/** List of elements that require raw contents and should not have SSR comments put in them */
export const RawTextElements = ['textarea', 'script', 'style', 'title'];
/** List of Element events that will be delegated */
export const DelegatedEvents = [
'beforeinput',

@ -177,8 +177,6 @@ export function flushSync(fn) {
flush_sync(fn);
}
export { unstate } from './internal/client/proxy.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export {

@ -33,14 +33,4 @@ export function unmount() {
export async function tick() {}
/**
* @template T
* @param {T} value
* @returns {T}
*/
export function unstate(value) {
// There's no signals/proxies on the server, so just return the value
return value;
}
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';

@ -0,0 +1,38 @@
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set_should_intro } from '../render.js';
import { get } from '../runtime.js';
/**
* @template {(anchor: Comment, props: any) => any} Component
* @param {import("#client").Source<Component>} source
*/
export function hmr(source) {
/**
* @param {Comment} anchor
* @param {any} props
*/
return (anchor, props) => {
let instance = {};
/** @type {import("#client").Effect} */
let effect;
block(() => {
const component = get(source);
if (effect) {
// @ts-ignore
for (var k in instance) delete instance[k];
destroy_effect(effect);
}
effect = branch(() => {
set_should_intro(false);
Object.assign(instance, component(anchor, props));
set_should_intro(true);
});
});
return instance;
};
}

@ -2,7 +2,6 @@
import { STATE_SYMBOL } from '../constants.js';
import { untrack } from '../runtime.js';
import { get_descriptors } from '../utils.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {};
@ -68,9 +67,8 @@ function get_component() {
* Together with `mark_module_end`, this function establishes the boundaries of a `.svelte` file,
* such that subsequent calls to `get_component` can tell us which component is responsible
* for a given state change
* @param {Function} component
*/
export function mark_module_start(component) {
export function mark_module_start() {
const start = get_stack()?.[2];
if (start) {
@ -78,121 +76,70 @@ export function mark_module_start(component) {
start,
// @ts-expect-error
end: null,
component
// @ts-expect-error we add the component at the end, since HMR will overwrite the function
component: null
});
}
}
export function mark_module_end() {
/**
* @param {Function} component
*/
export function mark_module_end(component) {
const end = get_stack()?.[2];
if (end) {
const boundaries_file = boundaries[end.file];
boundaries_file[boundaries_file.length - 1].end = end;
const boundary = boundaries_file[boundaries_file.length - 1];
boundary.end = end;
boundary.component = component;
}
}
let add_owner_visited = new Set();
/**
*
* @param {any} object
* @param {any} owner
*/
export function add_owner(object, owner) {
// Needed because ownership addition can invoke getters on a proxy,
// calling add_owner anew, so just keeping the set as part of
// add_owner_to_object would not be enough.
const prev = add_owner_visited;
try {
add_owner_visited = new Set(add_owner_visited);
untrack(() => {
add_owner_to_object(object, owner, add_owner_visited);
});
} finally {
add_owner_visited = prev;
}
untrack(() => {
add_owner_to_object(object, owner);
});
}
/**
* @param {any} object
* @param {Function} owner
* @param {Set<any>} visited
*/
function add_owner_to_object(object, owner, visited) {
if (visited.has(object)) return;
visited.add(object);
function add_owner_to_object(object, owner) {
if (object?.[STATE_SYMBOL]?.o && !object[STATE_SYMBOL].o.has(owner)) {
object[STATE_SYMBOL].o.add(owner);
for (const key in object) {
add_owner_to_object(object[key], owner);
}
}
// Not inside previous if-block; there could be normal objects in-between
traverse_for_owners(object, (nested) => add_owner_to_object(nested, owner, visited));
}
let strip_owner_visited = new Set();
/**
* @param {any} object
*/
export function strip_owner(object) {
// Needed because ownership stripping can invoke getters on a proxy,
// calling strip_owner anew, so just keeping the set as part of
// strip_owner_from_object would not be enough.
const prev = strip_owner_visited;
try {
untrack(() => {
strip_owner_from_object(object, strip_owner_visited);
});
} finally {
strip_owner_visited = prev;
}
untrack(() => {
strip_owner_from_object(object);
});
}
/**
* @param {any} object
* @param {Set<any>} visited
*/
function strip_owner_from_object(object, visited) {
if (visited.has(object)) return;
visited.add(object);
function strip_owner_from_object(object) {
if (object?.[STATE_SYMBOL]?.o) {
object[STATE_SYMBOL].o = null;
}
// Not inside previous if-block; there could be normal objects in-between
traverse_for_owners(object, (nested) => strip_owner_from_object(nested, visited));
}
/**
* @param {any} object
* @param {(obj: any) => void} cb
*/
function traverse_for_owners(object, cb) {
if (typeof object === 'object' && object !== null && !(object instanceof EventTarget)) {
for (const key in object) {
cb(object[key]);
}
// deal with state on classes
const proto = Object.getPrototypeOf(object);
if (
proto !== Object.prototype &&
proto !== Array.prototype &&
proto !== Map.prototype &&
proto !== Set.prototype &&
proto !== Date.prototype
) {
const descriptors = get_descriptors(proto);
for (let key in descriptors) {
const get = descriptors[key].get;
if (get) {
try {
cb(object[key]);
} catch (e) {
// continue
}
}
}
strip_owner(object[key]);
}
}
}

@ -23,12 +23,9 @@ import {
resume_effect
} from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen, map_get, map_set } from '../../utils.js';
import { is_array, is_frozen } from '../../utils.js';
import { STATE_SYMBOL } from '../../constants.js';
var NEW_ITEM = -1;
var LIS_ITEM = -2;
/**
* The row of a keyed each block that is currently updating. We track this
* so that `animate:` directives have something to attach themselves to
@ -41,25 +38,33 @@ export function set_current_each_item(item) {
current_each_item = item;
}
/**
* @param {any} _
* @param {number} i
*/
export function index(_, i) {
return i;
}
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {import('#client').Effect[]} effects
* @param {import('#client').EachItem[]} items
* @param {null | Node} controlled_anchor
* @param {() => void} [callback]
*/
function pause_effects(effects, controlled_anchor, callback) {
function pause_effects(items, controlled_anchor, callback) {
/** @type {import('#client').TransitionManager[]} */
var transitions = [];
var length = effects.length;
var length = items.length;
for (var i = 0; i < length; i++) {
pause_children(effects[i], transitions, true);
pause_children(items[i].e, transitions, true);
}
// If we have a controlled anchor, it means that the each block is inside a single
// DOM element, so we can apply a fast-path for clearing the contents of the element.
if (effects.length > 0 && transitions.length === 0 && controlled_anchor !== null) {
if (length > 0 && transitions.length === 0 && controlled_anchor !== null) {
var parent_node = /** @type {Element} */ (controlled_anchor.parentNode);
parent_node.textContent = '';
parent_node.append(controlled_anchor);
@ -67,7 +72,7 @@ function pause_effects(effects, controlled_anchor, callback) {
run_out_transitions(transitions, () => {
for (var i = 0; i < length; i++) {
destroy_effect(effects[i]);
destroy_effect(items[i].e);
}
if (callback !== undefined) callback();
@ -79,15 +84,14 @@ function pause_effects(effects, controlled_anchor, callback) {
* @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block
* @param {number} flags
* @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key
* @param {(value: V, index: number) => any} get_key
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @returns {void}
*/
function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_fn) {
export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
/** @type {import('#client').EachState} */
var state = { flags, items: [] };
var state = { flags, items: new Map(), next: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -113,8 +117,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
? []
: Array.from(collection);
var keys = get_key === null ? array : array.map(get_key);
var length = array.length;
// If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items
@ -145,11 +147,15 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
// this is separate to the previous block because `hydrating` might change
if (hydrating) {
var b_items = [];
/** @type {Node} */
var child_anchor = hydrate_nodes[0];
/** @type {import('#client').EachItem | import('#client').EachState} */
var prev = state;
/** @type {import('#client').EachItem} */
var item;
for (var i = 0; i < length; i++) {
if (
child_anchor.nodeType !== 8 ||
@ -163,8 +169,13 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
}
child_anchor = hydrate_anchor(child_anchor);
b_items[i] = create_item(child_anchor, array[i], keys?.[i], i, render_fn, flags);
var value = array[i];
var key = get_key(value, i);
item = create_item(child_anchor, prev, null, value, key, i, render_fn, flags);
state.items.set(key, item);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
prev = item;
}
// remove excess nodes
@ -175,12 +186,10 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
child_anchor = next;
}
}
state.items = b_items;
}
if (!hydrating) {
reconcile_fn(array, state, anchor, render_fn, flags, keys);
reconcile(array, state, anchor, render_fn, flags, get_key);
}
if (fallback_fn !== null) {
@ -204,33 +213,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
});
}
/**
* @template V
* @param {Element | Comment} anchor
* @param {number} flags
* @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} [fallback_fn]
* @returns {void}
*/
export function each_keyed(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_tracked_array);
}
/**
* @template V
* @param {Element | Comment} anchor
* @param {number} flags
* @param {() => V[]} get_collection
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} [fallback_fn]
* @returns {void}
*/
export function each_indexed(anchor, flags, get_collection, render_fn, fallback_fn = null) {
each(anchor, flags, get_collection, null, render_fn, fallback_fn, reconcile_indexed_array);
}
/**
* @template V
* @param {Array<V>} array
@ -238,308 +220,182 @@ export function each_indexed(anchor, flags, get_collection, render_fn, fallback_
* @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags
* @param {(value: V, index: number) => any} get_key
* @returns {void}
*/
function reconcile_indexed_array(array, state, anchor, render_fn, flags) {
var a_items = state.items;
function reconcile(array, state, anchor, render_fn, flags, get_key) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var a = a_items.length;
var b = array.length;
var min = Math.min(a, b);
var length = array.length;
var items = state.items;
var first = state.next;
var current = first;
/** @type {typeof a_items} */
var b_items = Array(b);
/** @type {Set<import('#client').EachItem>} */
var seen = new Set();
var item;
var value;
// update items
for (var i = 0; i < min; i += 1) {
value = array[i];
item = a_items[i];
b_items[i] = item;
update_item(item, value, i, flags);
resume_effect(item.e);
}
/** @type {import('#client').EachState | import('#client').EachItem} */
var prev = state;
if (b > a) {
// add items
for (; i < b; i += 1) {
value = array[i];
item = create_item(anchor, value, null, i, render_fn, flags);
b_items[i] = item;
}
state.items = b_items;
} else if (a > b) {
// remove items
var effects = [];
for (i = b; i < a; i += 1) {
effects.push(a_items[i].e);
}
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && b === 0 ? anchor : null;
/** @type {import('#client').EachItem[]} */
var to_animate = [];
pause_effects(effects, controlled_anchor, () => {
state.items.length = b;
});
}
}
/** @type {import('#client').EachItem[]} */
var matched = [];
/**
* Reconcile arrays by the equality of the elements in the array. This algorithm
* is based on Ivi's reconcilation logic:
* https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968
* @template V
* @param {Array<V>} array
* @param {import('#client').EachState} state
* @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags
* @param {any[]} keys
* @returns {void}
*/
function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
var a_items = state.items;
/** @type {import('#client').EachItem[]} */
var stashed = [];
var a = a_items.length;
var b = array.length;
/** @type {V} */
var value;
/** @type {Array<import('#client').EachItem>} */
var b_items = Array(b);
/** @type {any} */
var key;
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
var start = 0;
/** @type {import('#client').EachItem | undefined} */
var item;
/** @type {import('#client').Effect[]} */
var to_destroy = [];
/** @type {Array<import('#client').EachItem>} */
var to_animate = [];
// Step 1 — trim common suffix
while (a > 0 && b > 0 && a_items[a - 1].k === keys[b - 1]) {
item = b_items[--b] = a_items[--a];
anchor = get_first_child(item);
/** @type {number} */
var i;
resume_effect(item.e);
if (is_animated) {
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
item = items.get(key);
if (should_update) {
update_item(item, array[b], b, flags);
}
if (is_animated) {
item.a?.measure();
to_animate.push(item);
if (item !== undefined) {
item.a?.measure();
to_animate.push(item);
}
}
}
// Step 2 — trim common prefix
while (start < a && start < b && a_items[start].k === keys[start]) {
item = b_items[start] = a_items[start];
resume_effect(item.e);
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
item = items.get(key);
if (item === undefined) {
prev = create_item(
current ? get_first_child(current) : anchor,
prev,
prev.next,
value,
key,
i,
render_fn,
flags
);
items.set(key, prev);
matched = [];
stashed = [];
current = prev.next;
continue;
}
if (should_update) {
update_item(item, array[start], start, flags);
update_item(item, value, i, flags);
}
if (is_animated) {
item.a?.measure();
to_animate.push(item);
}
start += 1;
}
// Step 3 — update
if (start === a) {
// add only
while (start < b) {
item = create_item(anchor, array[start], keys[start], start, render_fn, flags);
b_items[start++] = item;
}
} else if (start === b) {
// remove only
while (start < a) {
to_destroy.push(a_items[start++].e);
}
} else {
// reconcile
var moved = false;
var sources = new Int32Array(b - start);
var indexes = new Map();
var i;
var index;
var last_item;
// store the indexes of each item in the new world
for (i = start; i < b; i += 1) {
sources[i - start] = NEW_ITEM;
map_set(indexes, keys[i], i);
}
resume_effect(item.e);
if (is_animated) {
// for all items that were in both the old and the new list,
// measure them and store them in `to_animate` so we can
// apply animations once the DOM has been updated
for (i = 0; i < a_items.length; i += 1) {
item = a_items[i];
if (indexes.has(item.k)) {
item.a?.measure();
to_animate.push(item);
}
}
}
if (item !== current) {
if (seen.has(item)) {
if (matched.length < stashed.length) {
// more efficient to move later items to the front
var start = stashed[0];
var local_anchor = get_first_child(start);
var j;
// populate the `sources` array for each old item with
// its new index, so that we can calculate moves
for (i = start; i < a; i += 1) {
item = a_items[i];
index = map_get(indexes, item.k);
prev = start.prev;
resume_effect(item.e);
var a = matched[0];
var b = matched[matched.length - 1];
if (index === undefined) {
to_destroy.push(item.e);
} else {
moved = true;
sources[index - start] = i;
b_items[index] = item;
link(a.prev, b.next);
link(prev, a);
link(b, start);
if (is_animated) {
to_animate.push(item);
}
}
}
for (j = 0; j < matched.length; j += 1) {
move(matched[j], local_anchor);
}
// if we need to move items (as opposed to just adding/removing),
// figure out how to do so efficiently (I would be lying if I said
// I fully understand this part)
if (moved) {
mark_lis(sources);
} else if (is_controlled && to_destroy.length === a_items.length) {
// We can optimize the case in which all items are replaced —
// destroy everything first, then append new items
pause_effects(to_destroy, anchor);
to_destroy = [];
}
for (j = 0; j < stashed.length; j += 1) {
seen.delete(stashed[j]);
}
// working from the back, insert new or moved items
while (b-- > start) {
index = sources[b - start];
var should_insert = index === NEW_ITEM;
if (should_insert) {
if (last_item !== undefined) anchor = get_first_child(last_item);
item = create_item(anchor, array[b], keys[b], b, render_fn, flags);
} else {
item = b_items[b];
if (should_update) {
update_item(item, array[b], b, flags);
}
current = start;
prev = b;
i -= 1;
if (moved && index !== LIS_ITEM) {
if (last_item !== undefined) anchor = get_first_child(last_item);
move(/** @type {import('#client').Dom} */ (item.e.dom), anchor);
}
}
matched = [];
stashed = [];
} else {
// more efficient to move earlier items to the back
seen.delete(item);
move(item, current ? get_first_child(current) : anchor);
last_item = b_items[b] = item;
}
}
link(item.prev, item.next);
link(item, prev.next);
link(prev, item);
if (to_animate.length > 0) {
// TODO we need to briefly take any outroing elements out of the flow, so that
// we can figure out the eventual destination of the animating elements
// - https://github.com/sveltejs/svelte/pull/10798#issuecomment-2013681778
// - https://svelte.dev/repl/6e891305e9644a7ca7065fa95c79d2d2?version=4.2.9
effect(() => {
untrack(() => {
for (item of to_animate) {
item.a?.apply();
prev = item;
}
});
});
}
var controlled_anchor = is_controlled && b_items.length === 0 ? anchor : null;
continue;
}
pause_effects(to_destroy, controlled_anchor, () => {
state.items = b_items;
});
}
matched = [];
stashed = [];
/**
* Longest Increased Subsequence algorithm
* @param {Int32Array} a
* @returns {void}
*/
function mark_lis(a) {
var length = a.length;
var parent = new Int32Array(length);
var index = new Int32Array(length);
var index_length = 0;
var i = 0;
/** @type {number} */
var j;
/** @type {number} */
var k;
while (current !== null && current.k !== key) {
seen.add(current);
stashed.push(current);
current = current.next;
}
/** @type {number} */
var lo;
if (current === null) {
continue;
}
/** @type {number} */
var hi;
item = current;
}
// Skip -1 values at the start of the input array `a`.
for (; a[i] === NEW_ITEM; ++i) {
/**/
matched.push(item);
prev = item;
current = item.next;
}
index[0] = i++;
for (; i < length; ++i) {
k = a[i];
if (k !== NEW_ITEM) {
// Ignore -1 values.
j = index[index_length];
const to_destroy = Array.from(seen);
if (a[j] < k) {
parent[i] = j;
index[++index_length] = i;
} else {
lo = 0;
hi = index_length;
while (current) {
to_destroy.push(current);
current = current.next;
}
while (lo < hi) {
j = (lo + hi) >> 1;
if (a[index[j]] < k) {
lo = j + 1;
} else {
hi = j;
}
}
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
if (k < a[index[lo]]) {
if (lo > 0) {
parent[i] = index[lo - 1];
}
index[lo] = i;
}
}
pause_effects(to_destroy, controlled_anchor, () => {
for (var i = 0; i < to_destroy.length; i += 1) {
var item = to_destroy[i];
items.delete(item.k);
link(item.prev, item.next);
}
}
// Mutate input array `a` and assign -2 value to all nodes that are part of LIS.
j = index[index_length];
});
while (index_length-- >= 0) {
a[j] = LIS_ITEM;
j = parent[j];
if (is_animated) {
effect(() => {
untrack(() => {
for (item of to_animate) {
item.a?.apply();
}
});
});
}
}
@ -579,6 +435,8 @@ function update_item(item, value, index, type) {
/**
* @template V
* @param {Node} anchor
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachItem | null} next
* @param {V} value
* @param {unknown} key
* @param {number} index
@ -586,7 +444,7 @@ function update_item(item, value, index, type) {
* @param {number} flags
* @returns {import('#client').EachItem}
*/
function create_item(anchor, value, key, index, render_fn, flags) {
function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
var previous_each_item = current_each_item;
try {
@ -603,9 +461,14 @@ function create_item(anchor, value, key, index, render_fn, flags) {
k: key,
a: null,
// @ts-expect-error
e: null
e: null,
prev,
next
};
prev.next = item;
if (next !== null) next.prev = item;
current_each_item = item;
item.e = branch(() => render_fn(anchor, v, i));
@ -616,15 +479,29 @@ function create_item(anchor, value, key, index, render_fn, flags) {
}
/**
* @param {import('#client').Dom} current
* @param {import('#client').EachItem} item
* @param {Text | Element | Comment} anchor
*/
function move(current, anchor) {
if (is_array(current)) {
for (var i = 0; i < current.length; i++) {
anchor.before(current[i]);
function move(item, anchor) {
var dom = item.e.dom;
if (dom !== null) {
if (is_array(dom)) {
for (var i = 0; i < dom.length; i++) {
anchor.before(dom[i]);
}
} else {
anchor.before(dom);
}
} else {
anchor.before(current);
}
}
/**
*
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachItem | null} next
*/
function link(prev, next) {
prev.next = next;
if (next !== null) next.prev = prev;
}

@ -104,6 +104,9 @@ export function set_attributes(element, prev, attrs, lowercase_attributes, css_h
var setters = map_get(setters_cache, element.nodeName);
if (!setters) map_set(setters_cache, element.nodeName, (setters = get_setters(element)));
// @ts-expect-error
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {});
for (key in next) {
var value = next[key];
if (value === prev?.[key]) continue;
@ -140,6 +143,7 @@ export function set_attributes(element, prev, attrs, lowercase_attributes, css_h
}
}
} else if (value == null) {
attributes[key] = null;
element.removeAttribute(key);
} else if (key === 'style') {
element.style.cssText = value + '';

@ -131,7 +131,6 @@ export function bind_checked(input, get_value, update) {
update(value);
});
// eslint-disable-next-line eqeqeq
if (get_value() == undefined) {
update(false);
}

@ -98,14 +98,14 @@ export function animation(element, get_fn, get_params) {
to = this.element.getBoundingClientRect();
const options = get_fn()(this.element, { from, to }, get_params?.());
if (
from.left !== to.left ||
from.right !== to.right ||
from.top !== to.top ||
from.bottom !== to.bottom
) {
const options = get_fn()(this.element, { from, to }, get_params?.());
animation = animate(this.element, options, undefined, 1, () => {
animation?.abort();
animation = undefined;

@ -1,9 +1,10 @@
export { hmr } from './dev/hmr.js';
export { add_owner, mark_module_start, mark_module_end } from './dev/ownership.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js';
export { css_props } from './dom/blocks/css-props.js';
export { each_keyed, each_indexed } from './dom/blocks/each.js';
export { index, each } from './dom/blocks/each.js';
export { html } from './dom/blocks/html.js';
export { snippet } from './dom/blocks/snippet.js';
export { component } from './dom/blocks/svelte-component.js';
@ -106,6 +107,7 @@ export {
update,
update_pre,
value_or_fallback,
value_or_fallback_async,
exclude_from_object,
pop,
push,
@ -126,7 +128,7 @@ export {
validate_store
} from './validate.js';
export { raf } from './timing.js';
export { proxy, unstate } from './proxy.js';
export { proxy, snapshot } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';
export {
child,

@ -19,7 +19,6 @@ import {
import { add_owner, check_ownership, strip_owner } from './dev/ownership.js';
import { mutable_source, source, set } from './reactivity/sources.js';
import { STATE_SYMBOL } from './constants.js';
import { updating_derived } from './reactivity/deriveds.js';
import { UNINITIALIZED } from '../../constants.js';
/**
@ -141,7 +140,7 @@ function unwrap(value, already_unwrapped) {
* @param {T} value
* @returns {T}
*/
export function unstate(value) {
export function snapshot(value) {
return /** @type {T} */ (
unwrap(/** @type {import('#client').ProxyStateObject} */ (value), new Map())
);
@ -207,13 +206,8 @@ const state_proxy_handler = {
const metadata = target[STATE_SYMBOL];
let s = metadata.s.get(prop);
// if we're reading a property in a reactive context, create a source,
// but only if it's an own property and not a prototype property
if (
s === undefined &&
(current_effect !== null || updating_derived) &&
(!(prop in target) || get_descriptor(target, prop)?.writable)
) {
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!(prop in target) || get_descriptor(target, prop)?.writable)) {
s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i, metadata.o));
metadata.s.set(prop, s);
}
@ -281,7 +275,7 @@ const state_proxy_handler = {
// 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
// object property before writing to that property.
if (s === undefined && current_effect !== null) {
if (s === undefined) {
// the read creates a signal
untrack(() => receiver[prop]);
s = metadata.s.get(prop);

@ -9,10 +9,8 @@ export function equals(value) {
* @returns {boolean}
*/
export function safe_not_equal(a, b) {
// eslint-disable-next-line eqeqeq
return a != a
? // eslint-disable-next-line eqeqeq
b == b
? b == b
: a !== b || (a !== null && typeof a === 'object') || typeof a === 'function';
}

@ -7,7 +7,7 @@ import {
init_operations
} from './dom/operations.js';
import { HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js';
import { flush_sync, push, pop, current_component_context, untrack } from './runtime.js';
import { flush_sync, push, pop, current_component_context } from './runtime.js';
import { effect_root, branch } from './reactivity/effects.js';
import {
hydrate_anchor,

@ -7,7 +7,7 @@ import {
object_freeze,
object_prototype
} from './utils.js';
import { unstate } from './proxy.js';
import { snapshot } from './proxy.js';
import { destroy_effect, effect, user_pre_effect } from './reactivity/effects.js';
import {
EFFECT,
@ -1025,11 +1025,21 @@ export function exclude_from_object(obj, keys) {
/**
* @template V
* @param {V} value
* @param {V} fallback
* @param {() => V} fallback lazy because could contain side effects
* @returns {V}
*/
export function value_or_fallback(value, fallback) {
return value === undefined ? fallback : value;
return value === undefined ? fallback() : value;
}
/**
* @template V
* @param {V} value
* @param {() => Promise<V>} fallback lazy because could contain side effects
* @returns {Promise<V>}
*/
export async function value_or_fallback_async(value, fallback) {
return value === undefined ? fallback() : value;
}
/**
@ -1166,26 +1176,26 @@ export function deep_read(value, visited = new Set()) {
}
/**
* Like `unstate`, but recursively traverses into normal arrays/objects to find potential states in them.
* Like `snapshot`, but recursively traverses into normal arrays/objects to find potential states in them.
* @param {any} value
* @param {Map<any, any>} visited
* @returns {any}
*/
function deep_unstate(value, visited = new Map()) {
function deep_snapshot(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = unstate(value);
const unstated = snapshot(value);
if (unstated !== value) {
visited.set(value, unstated);
return unstated;
}
const prototype = get_prototype_of(value);
// Only deeply unstate plain objects and arrays
// Only deeply snapshot plain objects and arrays
if (prototype === object_prototype || prototype === array_prototype) {
let contains_unstated = false;
/** @type {any} */
const nested_unstated = Array.isArray(value) ? [] : {};
for (let key in value) {
const result = deep_unstate(value[key], visited);
const result = deep_snapshot(value[key], visited);
nested_unstated[key] = result;
if (result !== value[key]) {
contains_unstated = true;
@ -1213,7 +1223,7 @@ export function inspect(get_value, inspect = console.log) {
user_pre_effect(() => {
const fn = () => {
const value = untrack(() => get_value().map((v) => deep_unstate(v)));
const value = untrack(() => get_value().map((v) => deep_snapshot(v)));
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
// eslint-disable-next-line no-console
console.warn(
@ -1301,9 +1311,9 @@ if (DEV) {
*/
export function freeze(value) {
if (typeof value === 'object' && value != null && !is_frozen(value)) {
// If the object is already proxified, then unstate the value
// If the object is already proxified, then snapshot the value
if (STATE_SYMBOL in value) {
return object_freeze(unstate(value));
return object_freeze(snapshot(value));
}
// Otherwise freeze the object
object_freeze(value);

@ -50,8 +50,10 @@ export type Dom = TemplateNode | TemplateNode[];
export type EachState = {
/** flags */
flags: number;
/** items */
items: EachItem[];
/** a key -> item lookup */
items: Map<any, EachItem>;
/** head of the linked list of items */
next: EachItem | null;
};
export type EachItem = {
@ -65,6 +67,8 @@ export type EachItem = {
i: number | Source<number>;
/** key */
k: unknown;
prev: EachItem | EachState;
next: EachItem | null;
};
export interface TransitionManager {

@ -3,6 +3,7 @@ import { subscribe_to_store } from '../../store/utils.js';
import {
UNINITIALIZED,
DOMBooleanAttributes,
RawTextElements,
disallowed_paragraph_contents,
interactive_elements,
is_tag_valid_with_parent
@ -161,11 +162,11 @@ export function element(payload, tag, attributes_fn, children_fn) {
payload.out += `>`;
if (!VoidElements.has(tag)) {
if (tag !== 'textarea') {
if (!RawTextElements.includes(tag)) {
payload.out += BLOCK_OPEN;
}
children_fn();
if (tag !== 'textarea') {
if (!RawTextElements.includes(tag)) {
payload.out += BLOCK_CLOSE;
}
payload.out += `</${tag}>`;
@ -513,11 +514,21 @@ export function unsubscribe_stores(store_values) {
/**
* @template V
* @param {V} value
* @param {V} fallback
* @param {() => V} fallback lazy because could contain side effects
* @returns {V}
*/
export function value_or_fallback(value, fallback) {
return value === undefined ? fallback : value;
return value === undefined ? fallback() : value;
}
/**
* @template V
* @param {V} value
* @param {() => Promise<V>} fallback lazy because could contain side effects
* @returns {Promise<V>}
*/
export async function value_or_fallback_async(value, fallback) {
return value === undefined ? fallback() : value;
}
/**

@ -1,4 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-empty-function
export const noop = () => {};
// Adapted from https://github.com/then/is-promise/blob/master/index.js

@ -53,7 +53,7 @@ export function asClassComponent(component) {
class Svelte4Component {
/** @type {any} */
#events = {};
#events;
/** @type {Record<string, any>} */
#instance;
@ -70,7 +70,7 @@ class Svelte4Component {
// Using proxy state here isn't completely mirroring the Svelte 4 behavior, because mutations to a property
// cause fine-grained updates to only the places where that property is used, and not the entire property.
// Reactive statements and actions (the things where this matters) are handling this properly regardless, so it should be fine in practise.
const props = proxy({ ...(options.props || {}), $$events: this.#events }, false);
const props = proxy({ ...(options.props || {}), $$events: {} }, false);
this.#instance = (options.hydrate ? hydrate : mount)(options.component, {
target: options.target,
props,
@ -79,6 +79,8 @@ class Svelte4Component {
recover: options.recover
});
this.#events = props.$$events;
for (const key of Object.keys(this.#instance)) {
if (key === '$set' || key === '$destroy' || key === '$on') continue;
define_property(this, key, {

@ -1,3 +1,4 @@
export { ReactiveDate as Date } from './date.js';
export { ReactiveSet as Set } from './set.js';
export { ReactiveMap as Map } from './map.js';
export { ReactiveURL as URL, ReactiveURLSearchParams as URLSearchParams } from './url.js';

@ -1,3 +1,5 @@
export const Date = globalThis.Date;
export const Set = globalThis.Set;
export const Map = globalThis.Map;
export const URL = globalThis.URL;
export const URLSearchParams = globalThis.URLSearchParams;

@ -136,14 +136,15 @@ export class ReactiveMap extends Map {
values() {
get(this.#version);
return map(this.#sources.values(), get);
return map(this.#sources.values(), get, 'Map Iterator');
}
entries() {
get(this.#version);
return map(
this.#sources.entries(),
([key, source]) => /** @type {[K, V]} */ ([key, get(source)])
([key, source]) => /** @type {[K, V]} */ ([key, get(source)]),
'Map Iterator'
);
}

@ -47,10 +47,7 @@ export class ReactiveSet extends Set {
var proto = ReactiveSet.prototype;
var set_proto = Set.prototype;
/** @type {string} */
var method;
for (method of read_methods) {
for (const method of read_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
@ -59,7 +56,7 @@ export class ReactiveSet extends Set {
};
}
for (method of set_like_methods) {
for (const method of set_like_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
@ -135,7 +132,7 @@ export class ReactiveSet extends Set {
keys() {
get(this.#version);
return this.#sources.keys();
return map(this.#sources.keys(), (key) => key, 'Set Iterator');
}
values() {
@ -143,7 +140,7 @@ export class ReactiveSet extends Set {
}
entries() {
return map(this.keys(), (key) => /** @type {[T, T]} */ ([key, key]));
return map(this.keys(), (key) => /** @type {[T, T]} */ ([key, key]), 'Set Iterator');
}
[Symbol.iterator]() {

@ -0,0 +1,268 @@
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
const REPLACE = Symbol();
export class ReactiveURL extends URL {
#protocol = source(super.protocol);
#username = source(super.username);
#password = source(super.password);
#hostname = source(super.hostname);
#port = source(super.port);
#pathname = source(super.pathname);
#hash = source(super.hash);
#searchParams = new ReactiveURLSearchParams();
/**
* @param {string | URL} url
* @param {string | URL} [base]
*/
constructor(url, base) {
url = new URL(url, base);
super(url);
this.#searchParams[REPLACE](url.searchParams);
}
get hash() {
return get(this.#hash);
}
set hash(value) {
super.hash = value;
set(this.#hash, super.hash);
}
get host() {
get(this.#hostname);
get(this.#port);
return super.host;
}
set host(value) {
super.host = value;
set(this.#hostname, super.hostname);
set(this.#port, super.port);
}
get hostname() {
return get(this.#hostname);
}
set hostname(value) {
super.hostname = value;
set(this.#hostname, super.hostname);
}
get href() {
get(this.#protocol);
get(this.#username);
get(this.#password);
get(this.#hostname);
get(this.#port);
get(this.#pathname);
get(this.#hash);
this.#searchParams.toString();
return super.href;
}
set href(value) {
super.href = value;
set(this.#protocol, super.protocol);
set(this.#username, super.username);
set(this.#password, super.password);
set(this.#hostname, super.hostname);
set(this.#port, super.port);
set(this.#pathname, super.pathname);
set(this.#hash, super.hash);
this.#searchParams[REPLACE](super.searchParams);
}
get password() {
return get(this.#password);
}
set password(value) {
super.password = value;
set(this.#password, super.password);
}
get pathname() {
return get(this.#pathname);
}
set pathname(value) {
super.pathname = value;
set(this.#pathname, super.pathname);
}
get port() {
return get(this.#port);
}
set port(value) {
super.port = value;
set(this.#port, super.port);
}
get protocol() {
return get(this.#protocol);
}
set protocol(value) {
super.protocol = value;
set(this.#protocol, super.protocol);
}
get search() {
const search = this.#searchParams?.toString();
return search ? `?${search}` : '';
}
set search(value) {
super.search = value;
this.#searchParams[REPLACE](new URLSearchParams(value.replace(/^\?/, '')));
}
get username() {
return get(this.#username);
}
set username(value) {
super.username = value;
set(this.#username, super.username);
}
get origin() {
get(this.#protocol);
get(this.#hostname);
get(this.#port);
return super.origin;
}
get searchParams() {
return this.#searchParams;
}
toString() {
return this.href;
}
toJSON() {
return this.href;
}
}
export class ReactiveURLSearchParams extends URLSearchParams {
#version = source(0);
#increment_version() {
set(this.#version, this.#version.v + 1);
}
/**
* @param {URLSearchParams} params
*/
[REPLACE](params) {
for (const key of [...super.keys()]) {
super.delete(key);
}
for (const [key, value] of params) {
super.append(key, value);
}
this.#increment_version();
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
append(name, value) {
this.#increment_version();
return super.append(name, value);
}
/**
* @param {string} name
* @param {string=} value
* @returns {void}
*/
delete(name, value) {
this.#increment_version();
return super.delete(name, value);
}
/**
* @param {string} name
* @returns {string|null}
*/
get(name) {
get(this.#version);
return super.get(name);
}
/**
* @param {string} name
* @returns {string[]}
*/
getAll(name) {
get(this.#version);
return super.getAll(name);
}
/**
* @param {string} name
* @param {string=} value
* @returns {boolean}
*/
has(name, value) {
get(this.#version);
return super.has(name, value);
}
keys() {
get(this.#version);
return super.keys();
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
set(name, value) {
this.#increment_version();
return super.set(name, value);
}
sort() {
this.#increment_version();
return super.sort();
}
toString() {
get(this.#version);
return super.toString();
}
values() {
get(this.#version);
return super.values();
}
entries() {
get(this.#version);
return super.entries();
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
get(this.#version);
return super.size;
}
}

@ -0,0 +1,101 @@
import { render_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../index-client.js';
import { ReactiveURL, ReactiveURLSearchParams } from './url.js';
import { assert, test } from 'vitest';
test('url.hash', () => {
const url = new ReactiveURL('http://google.com');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(url.hash);
});
});
flushSync(() => {
url.hash = 'abc';
});
flushSync(() => {
url.href = 'http://google.com/a/b/c#def';
});
flushSync(() => {
// does not affect hash
url.pathname = 'e/f';
});
assert.deepEqual(log, ['', '#abc', '#def']);
cleanup();
});
test('url.searchParams', () => {
const url = new ReactiveURL('https://svelte.dev?foo=bar&t=123');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push('search: ' + url.search);
});
render_effect(() => {
log.push('foo: ' + url.searchParams.get('foo'));
});
render_effect(() => {
log.push('q: ' + url.searchParams.has('q'));
});
});
flushSync(() => {
url.search = '?q=kit&foo=baz';
});
flushSync(() => {
url.searchParams.append('foo', 'qux');
});
flushSync(() => {
url.searchParams.delete('foo');
});
assert.deepEqual(log, [
'search: ?foo=bar&t=123',
'foo: bar',
'q: false',
'search: ?q=kit&foo=baz',
'foo: baz',
'q: true',
'search: ?q=kit&foo=baz&foo=qux',
'foo: baz',
'q: true',
'search: ?q=kit',
'foo: null',
'q: true'
]);
cleanup();
});
test('URLSearchParams', () => {
const params = new ReactiveURLSearchParams();
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.set('a', 'b');
});
flushSync(() => {
params.append('a', 'c');
});
assert.deepEqual(log, ['', 'a=b', 'a=b&a=c']);
cleanup();
});

@ -3,9 +3,10 @@
* @template U
* @param {Iterable<T>} iterable
* @param {(value: T) => U} fn
* @param {string} name
* @returns {IterableIterator<U>}
*/
export function map(iterable, fn) {
export function map(iterable, fn, name) {
return {
[Symbol.iterator]: get_this,
next() {
@ -14,6 +15,10 @@ export function map(iterable, fn) {
}
return { done: true, value: undefined };
},
// @ts-expect-error
get [Symbol.toStringTag]() {
return name;
}
};
}

@ -27,11 +27,7 @@ export function readable(value, start) {
* @returns {boolean}
*/
export function safe_not_equal(a, b) {
// eslint-disable-next-line eqeqeq
return a != a
? // eslint-disable-next-line eqeqeq
b == b
: a !== b || (a && typeof a === 'object') || typeof a === 'function';
return a != a ? b == b : a !== b || (a && typeof a === 'object') || typeof a === 'function';
}
/**

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

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-sequence-expression',
message:
'Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses',
position: [124, 131]
}
});

@ -0,0 +1,9 @@
<script>
let { x, y, z } = $props();
</script>
<!-- allowed -->
<span foo={(x, y, z)} />
<!-- not allowed -->
<span foo={x, y, z} />

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-snippet-assignment',
message: 'Cannot reassign or bind to snippet parameter'
}
});

@ -0,0 +1,3 @@
{#snippet foo(value)}
<input bind:value={value}>
{/snippet}

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-snippet-assignment',
message: 'Cannot reassign or bind to snippet parameter'
}
});

@ -0,0 +1,3 @@
{#snippet foo(value)}
<button onclick={() => value += 1}>click</button>
{/snippet}

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-rune-args-length',
message: '$state.snapshot can only be called with 1 argument'
}
});

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'illegal-store-subscription',
message: 'Cannot subscribe to stores that are not declared at the top level of the component',
position: [142, 147]
}
});

@ -0,0 +1,8 @@
<script>
import { writable } from 'svelte/store';
const user = writable(0);
const users = [];
$: selected = users.find(user => user.id == $user);
$: console.log($user);
</script>

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'illegal-store-subscription',
message: 'Cannot subscribe to stores that are not declared at the top level of the component',
position: [167, 172]
}
});

@ -0,0 +1,8 @@
<script>
import { writable } from 'svelte/store';
const user = writable(0);
const users = [];
$: console.log($user);
$: selected = users.find(user => user.id == $user);
</script>

@ -78,8 +78,8 @@
"content": {
"start": 23,
"end": 48,
"comment": null,
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n",
"comment": null
}
}
}

@ -78,8 +78,8 @@
"content": {
"start": 23,
"end": 48,
"comment": null,
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n",
"comment": null
}
}
}

@ -1074,8 +1074,8 @@
"content": {
"start": 7,
"end": 798,
"comment": null,
"styles": "\n /* test that all these are parsed correctly */\n\th1:nth-of-type(2n+1){\n background: red;\n }\n h1:nth-child(-n + 3 of li.important) {\n background: red;\n }\n h1:nth-child(1) {\n background: red;\n }\n h1:nth-child(p) {\n background: red;\n }\n h1:nth-child(n+7) {\n background: red;\n }\n h1:nth-child(even) {\n background: red;\n }\n h1:nth-child(odd) {\n background: red;\n }\n h1:nth-child(\n n\n ) {\n background: red;\n }\n h1:global(nav) {\n background: red;\n }\n\t\th1:nth-of-type(10n+1){\n background: red;\n }\n\t\th1:nth-of-type(-2n+3){\n background: red;\n }\n\t\th1:nth-of-type(+12){\n background: red;\n }\n\t\th1:nth-of-type(+3n){\n background: red;\n }\n"
"styles": "\n /* test that all these are parsed correctly */\n\th1:nth-of-type(2n+1){\n background: red;\n }\n h1:nth-child(-n + 3 of li.important) {\n background: red;\n }\n h1:nth-child(1) {\n background: red;\n }\n h1:nth-child(p) {\n background: red;\n }\n h1:nth-child(n+7) {\n background: red;\n }\n h1:nth-child(even) {\n background: red;\n }\n h1:nth-child(odd) {\n background: red;\n }\n h1:nth-child(\n n\n ) {\n background: red;\n }\n h1:global(nav) {\n background: red;\n }\n\t\th1:nth-of-type(10n+1){\n background: red;\n }\n\t\th1:nth-of-type(-2n+3){\n background: red;\n }\n\t\th1:nth-of-type(+12){\n background: red;\n }\n\t\th1:nth-of-type(+3n){\n background: red;\n }\n",
"comment": null
}
},
"js": [],

@ -393,8 +393,8 @@
"content": {
"start": 7,
"end": 378,
"comment": null,
"styles": "\n /* test that all these are parsed correctly */\n\t::view-transition-old(x-y) {\n\t\tcolor: red;\n }\n\t:global(::view-transition-old(x-y)) {\n\t\tcolor: red;\n }\n\t::highlight(rainbow-color-1) {\n\t\tcolor: red;\n\t}\n\tcustom-element::part(foo) {\n\t\tcolor: red;\n\t}\n\t::slotted(.content) {\n\t\tcolor: red;\n\t}\n\t:is( /*button*/\n\t\tbutton, /*p after h1*/\n\t\th1 + p\n\t\t){\n\t\tcolor: red;\n\t}\n"
"styles": "\n /* test that all these are parsed correctly */\n\t::view-transition-old(x-y) {\n\t\tcolor: red;\n }\n\t:global(::view-transition-old(x-y)) {\n\t\tcolor: red;\n }\n\t::highlight(rainbow-color-1) {\n\t\tcolor: red;\n\t}\n\tcustom-element::part(foo) {\n\t\tcolor: red;\n\t}\n\t::slotted(.content) {\n\t\tcolor: red;\n\t}\n\t:is( /*button*/\n\t\tbutton, /*p after h1*/\n\t\th1 + p\n\t\t){\n\t\tcolor: red;\n\t}\n",
"comment": null
}
},
"js": [],

@ -71,8 +71,8 @@
"content": {
"start": 43,
"end": 197,
"comment": null,
"styles": "\n\t@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap\");\n\th1 {\n\t\tfont-weight: bold;\n\t\tbackground: url(\"whatever\");\n\t}\n"
"styles": "\n\t@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap\");\n\th1 {\n\t\tfont-weight: bold;\n\t\tbackground: url(\"whatever\");\n\t}\n",
"comment": null
}
},
"js": [],

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.htmlEqual(target.innerHTML, `hello world`);
}
});

@ -0,0 +1,11 @@
<script>
import { getContext, setContext } from 'svelte';
setContext('val', 'hello world')
const get_val = () => {
return getContext('val');
}
</script>
{get_val()}

@ -10,7 +10,7 @@ export default test({
intro: true,
test({ assert, component, target, raf }) {
let divs = /** @type {NodeListOf<HTMLDivElement & { foo: number, i: number }>} */ (
const divs = /** @type {NodeListOf<HTMLDivElement & { foo: number, i: number }>} */ (
target.querySelectorAll('div')
);
divs[0].i = 0; // for debugging
@ -43,20 +43,17 @@ export default test({
raf.tick(175);
assert.equal(divs[0].foo, 1);
// Svelte 5, we no longer revert previous reconciled items
// assert.equal(divs[1].foo, 0.75);
assert.equal(divs[1].foo, 0.75);
assert.equal(divs[2].foo, 1);
raf.tick(225);
const divs3 = target.querySelectorAll('div');
assert.strictEqual(divs[0], divs3[0]);
// Svelte 5, we no longer revert previous reconciled items
// assert.strictEqual(divs[1], divs3[1]);
assert.strictEqual(divs[1], divs3[1]);
assert.strictEqual(divs[2], divs3[2]);
assert.equal(divs[0].foo, 1);
// Svelte 5, we no longer revert previous reconciled items
// assert.equal(divs[1].foo, 1);
assert.equal(divs[1].foo, 1);
assert.equal(divs[2].foo, 1);
}
});

@ -35,9 +35,7 @@ export default test({
const btn = target.querySelector('button');
const clickEvent = new window.Event('click', { bubbles: true });
await btn?.dispatchEvent(clickEvent);
for (let i = 1; i <= 42; i += 1) {
await Promise.resolve();
}
await new Promise((r) => setTimeout(() => r(0), 100));
assert.htmlEqual(
target.innerHTML,

@ -0,0 +1,13 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: false // or else the arg will be called eagerly anyway to check for dead zones
},
html: `
<div>1 1 1</div>
<div>2 2 2</div>
<div>1 1 1</div>
<p>2</p>
`
});

@ -0,0 +1,14 @@
<script>
import { untrack } from 'svelte';
let count = $state(0);
function default_arg() {
untrack(() => count++);
return 1;
}
</script>
{#each [{}, { a: 2 }, {}] as { a = default_arg() }}
<div>{a} {a} {a}</div>
{/each}
<p>{count}</p>

@ -1,35 +0,0 @@
import { tick } from 'svelte';
import { test } from '../../test';
/** @type {typeof console.warn} */
let warn;
/** @type {any[]} */
let warnings = [];
export default test({
compileOptions: {
dev: true
},
before_test: () => {
warn = console.warn;
console.warn = (...args) => {
warnings.push(...args);
};
},
after_test: () => {
console.warn = warn;
warnings = [];
},
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
await tick();
assert.deepEqual(warnings.length, 0);
}
});

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

Loading…
Cancel
Save