diff --git a/.changeset/curvy-ties-shout.md b/.changeset/curvy-ties-shout.md new file mode 100644 index 0000000000..ee8bc567e3 --- /dev/null +++ b/.changeset/curvy-ties-shout.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: deeply unstate objects passed to inspect diff --git a/.changeset/dirty-bats-punch.md b/.changeset/dirty-bats-punch.md new file mode 100644 index 0000000000..8274a2bbc3 --- /dev/null +++ b/.changeset/dirty-bats-punch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle delegated events of elements moved outside the container diff --git a/.changeset/dry-eggs-play.md b/.changeset/dry-eggs-play.md new file mode 100644 index 0000000000..962e26fbff --- /dev/null +++ b/.changeset/dry-eggs-play.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve text node output diff --git a/.changeset/dry-eggs-retire.md b/.changeset/dry-eggs-retire.md new file mode 100644 index 0000000000..1e8bb95d75 --- /dev/null +++ b/.changeset/dry-eggs-retire.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve style parser whitespace handling diff --git a/.changeset/good-cars-visit.md b/.changeset/good-cars-visit.md new file mode 100644 index 0000000000..38e71d6633 --- /dev/null +++ b/.changeset/good-cars-visit.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow input elements within button elements diff --git a/.changeset/hip-balloons-begin.md b/.changeset/hip-balloons-begin.md new file mode 100644 index 0000000000..413b254065 --- /dev/null +++ b/.changeset/hip-balloons-begin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve script `lang` attribute detection diff --git a/.changeset/light-humans-hang.md b/.changeset/light-humans-hang.md new file mode 100644 index 0000000000..de1f6ed8ff --- /dev/null +++ b/.changeset/light-humans-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: support TypeScript's `satisfies` operator diff --git a/.changeset/nasty-lions-double.md b/.changeset/nasty-lions-double.md new file mode 100644 index 0000000000..404e08168d --- /dev/null +++ b/.changeset/nasty-lions-double.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve pseudo class parsing diff --git a/.changeset/old-houses-drum.md b/.changeset/old-houses-drum.md new file mode 100644 index 0000000000..78f26ee549 --- /dev/null +++ b/.changeset/old-houses-drum.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: provide `unstate` in server environment diff --git a/.changeset/old-oranges-compete.md b/.changeset/old-oranges-compete.md new file mode 100644 index 0000000000..cf5b699546 --- /dev/null +++ b/.changeset/old-oranges-compete.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve key block reactivity detection diff --git a/.changeset/orange-dingos-poke.md b/.changeset/orange-dingos-poke.md new file mode 100644 index 0000000000..3e9e6b8ade --- /dev/null +++ b/.changeset/orange-dingos-poke.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add types for popover attributes and events diff --git a/.changeset/pre.json b/.changeset/pre.json index f2b203735f..e698b5f510 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -25,9 +25,13 @@ "cool-ants-leave", "cuddly-pianos-drop", "curly-lizards-dream", + "curvy-ties-shout", + "dirty-bats-punch", "dirty-garlics-design", "dirty-tips-add", "dry-clocks-grow", + "dry-eggs-play", + "dry-eggs-retire", "dull-mangos-wave", "early-ads-tie", "eight-steaks-shout", @@ -47,6 +51,7 @@ "funny-wombats-argue", "gentle-sheep-hug", "giant-roses-press", + "good-cars-visit", "good-pianos-jump", "great-icons-retire", "green-eggs-approve", @@ -54,6 +59,7 @@ "happy-suits-film", "healthy-planes-vanish", "heavy-ears-rule", + "hip-balloons-begin", "honest-icons-change", "hungry-dots-fry", "hungry-tips-unite", @@ -71,6 +77,7 @@ "lazy-months-knock", "lazy-spiders-think", "lemon-geese-drum", + "light-humans-hang", "light-pens-watch", "long-buckets-lay", "long-crews-return", @@ -80,6 +87,7 @@ "lucky-schools-hang", "moody-frogs-exist", "moody-owls-cry", + "nasty-lions-double", "neat-dingos-clap", "new-boats-wait", "ninety-dingos-walk", @@ -87,13 +95,17 @@ "odd-schools-wait", "odd-shoes-cheat", "old-flies-jog", + "old-houses-drum", "old-mails-sneeze", + "old-oranges-compete", + "orange-dingos-poke", "polite-dolphins-care", "polite-pumpkins-guess", "polite-ravens-study", "poor-eggs-enjoy", "poor-seahorses-flash", "popular-mangos-rest", + "pretty-ties-help", "purple-dragons-peel", "quiet-camels-mate", "rare-pears-whisper", @@ -108,6 +120,7 @@ "seven-deers-jam", "seven-ravens-check", "sharp-gorillas-impress", + "sharp-kids-happen", "sharp-tomatoes-learn", "shiny-baboons-play", "shiny-shrimps-march", @@ -125,10 +138,12 @@ "strong-gifts-smoke", "strong-lemons-provide", "sweet-mangos-beg", + "sweet-pens-sniff", "swift-donkeys-perform", "swift-ravens-hunt", "swift-seahorses-deliver", "tall-books-grin", + "tall-garlics-try", "tall-shrimps-worry", "tall-tigers-wait", "tasty-numbers-perform", @@ -140,12 +155,14 @@ "thirty-ghosts-fix", "thirty-impalas-repair", "thirty-wombats-relax", + "three-suits-grin", "tiny-kings-whisper", "twelve-dragons-join", "twelve-onions-juggle", "two-dragons-yell", "two-falcons-buy", "unlucky-boxes-obey", + "unlucky-trees-lick", "wet-games-fly", "wicked-clouds-exercise", "wicked-doors-train", diff --git a/.changeset/pretty-ties-help.md b/.changeset/pretty-ties-help.md new file mode 100644 index 0000000000..f2408416d4 --- /dev/null +++ b/.changeset/pretty-ties-help.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip generating $.proxy() calls for unary and binary expressions diff --git a/.changeset/sharp-kids-happen.md b/.changeset/sharp-kids-happen.md new file mode 100644 index 0000000000..88b5badb15 --- /dev/null +++ b/.changeset/sharp-kids-happen.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always treat spread attributes as reactive and separate them if needed diff --git a/.changeset/sweet-pens-sniff.md b/.changeset/sweet-pens-sniff.md new file mode 100644 index 0000000000..165c1e05f7 --- /dev/null +++ b/.changeset/sweet-pens-sniff.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow pseudo classes after `:global(..)` diff --git a/.changeset/tall-garlics-try.md b/.changeset/tall-garlics-try.md new file mode 100644 index 0000000000..a7b45bdf69 --- /dev/null +++ b/.changeset/tall-garlics-try.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: bail-out event handler referencing each index diff --git a/.changeset/three-suits-grin.md b/.changeset/three-suits-grin.md new file mode 100644 index 0000000000..7fa0154e5f --- /dev/null +++ b/.changeset/three-suits-grin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: parse `:nth-of-type(xn+y)` correctly diff --git a/.changeset/unlucky-trees-lick.md b/.changeset/unlucky-trees-lick.md new file mode 100644 index 0000000000..6e6f68b7c6 --- /dev/null +++ b/.changeset/unlucky-trees-lick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure if block is executed in correct order diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08820f0ed3..7c3969cd54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,4 +50,11 @@ jobs: with: node-version: 18 cache: pnpm - - run: 'pnpm i && pnpm check && pnpm lint' + - name: install + run: pnpm install --frozen-lockfile + - name: type check + run: pnpm check + - name: lint + run: pnpm lint + - name: build and check generated types + run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed β€” please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24b1229b42..7810870670 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,11 @@ jobs: node-version: 18.x cache: pnpm - - run: pnpm install --frozen-lockfile + - name: Install + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed β€” please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); } - name: Create Release Pull Request or Publish to npm id: changesets diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 167a62646e..613551574f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -133,6 +133,10 @@ To typecheck the codebase, run `pnpm check` inside `packages/svelte`. To typeche - `snake_case` for internal variable names and methods. - `camelCase` for public variable names and methods. +### Generating types + +Types are auto-generated from the source, but the result is checked in to ensure no accidental changes slip through. Run `pnpm generate:types` to regenerate the types. + ### Sending your pull request Please make sure the following is done when submitting a pull request: @@ -141,7 +145,7 @@ Please make sure the following is done when submitting a pull request: 1. Make sure your code lints (`pnpm lint`). 1. Make sure your tests pass (`pnpm test`). -All pull requests should be opened against the `main` branch. Make sure the PR does only one thing, otherwise please split it. +All pull requests should be opened against the `main` branch. Make sure the PR does only one thing, otherwise please split it. If this change should contribute to a version bump, run `npx changeset` at the root of the repository after a code change and select the appropriate packages. #### Breaking changes diff --git a/LICENSE.md b/LICENSE.md index aa74406768..e2a8b89fa4 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2016-23 [these people](https://github.com/sveltejs/svelte/graphs/contributors) +Copyright (c) 2016-24 [these people](https://github.com/sveltejs/svelte/graphs/contributors) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/documentation/examples/21-miscellaneous/02-immutable-data/MutableTodo.svelte b/documentation/examples/21-miscellaneous/02-immutable-data/MutableTodo.svelte index fb960ead91..d2ad294728 100644 --- a/documentation/examples/21-miscellaneous/02-immutable-data/MutableTodo.svelte +++ b/documentation/examples/21-miscellaneous/02-immutable-data/MutableTodo.svelte @@ -13,7 +13,7 @@ - diff --git a/packages/svelte/.gitignore b/packages/svelte/.gitignore index faa8892951..e4925570e5 100644 --- a/packages/svelte/.gitignore +++ b/packages/svelte/.gitignore @@ -1,4 +1,5 @@ -/types +/types/*.map +/types/compiler /compiler.cjs /action.d.ts diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 0f9afb242d..57240fb631 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,47 @@ # svelte +## 5.0.0-next.29 + +### Patch Changes + +- fix: improve text node output ([#10081](https://github.com/sveltejs/svelte/pull/10081)) + +- fix: improve style parser whitespace handling ([#10077](https://github.com/sveltejs/svelte/pull/10077)) + +- fix: allow input elements within button elements ([#10083](https://github.com/sveltejs/svelte/pull/10083)) + +- fix: support TypeScript's `satisfies` operator ([#10068](https://github.com/sveltejs/svelte/pull/10068)) + +- fix: provide `unstate` in server environment ([`877ff1ee7`](https://github.com/sveltejs/svelte/commit/877ff1ee7d637e2248145d975748e1012a977396)) + +- fix: improve key block reactivity detection ([#10092](https://github.com/sveltejs/svelte/pull/10092)) + +- fix: always treat spread attributes as reactive and separate them if needed ([#10071](https://github.com/sveltejs/svelte/pull/10071)) + +## 5.0.0-next.28 + +### Patch Changes + +- fix: deeply unstate objects passed to inspect ([#10056](https://github.com/sveltejs/svelte/pull/10056)) + +- fix: handle delegated events of elements moved outside the container ([#10060](https://github.com/sveltejs/svelte/pull/10060)) + +- fix: improve script `lang` attribute detection ([#10046](https://github.com/sveltejs/svelte/pull/10046)) + +- fix: improve pseudo class parsing ([#10055](https://github.com/sveltejs/svelte/pull/10055)) + +- fix: add types for popover attributes and events ([#10041](https://github.com/sveltejs/svelte/pull/10041)) + +- fix: skip generating $.proxy() calls for unary and binary expressions ([#9979](https://github.com/sveltejs/svelte/pull/9979)) + +- fix: allow pseudo classes after `:global(..)` ([#10055](https://github.com/sveltejs/svelte/pull/10055)) + +- fix: bail-out event handler referencing each index ([#10063](https://github.com/sveltejs/svelte/pull/10063)) + +- fix: parse `:nth-of-type(xn+y)` correctly ([#9970](https://github.com/sveltejs/svelte/pull/9970)) + +- fix: ensure if block is executed in correct order ([#10053](https://github.com/sveltejs/svelte/pull/10053)) + ## 5.0.0-next.27 ### Patch Changes diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 41b0f07bf8..d153de10db 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -59,6 +59,7 @@ export type WheelEventHandler = EventHandler = EventHandler; export type TransitionEventHandler = EventHandler; export type MessageEventHandler = EventHandler; +export type ToggleEventHandler = EventHandler; // // DOM Attributes @@ -136,10 +137,13 @@ export interface DOMAttributes { onerror?: EventHandler | undefined | null; // also a Media Event onerrorcapture?: EventHandler | undefined | null; // also a Media Event - // Detail Events - 'on:toggle'?: EventHandler | undefined | null; - ontoggle?: EventHandler | undefined | null; - ontogglecapture?: EventHandler | undefined | null; + // Popover Events + 'on:beforetoggle'?: ToggleEventHandler | undefined | null; + onbeforetoggle?: ToggleEventHandler | undefined | null; + onbeforetogglecapture?: ToggleEventHandler | undefined | null; + 'on:toggle'?: ToggleEventHandler | undefined | null; + ontoggle?: ToggleEventHandler | undefined | null; + ontogglecapture?: ToggleEventHandler | undefined | null; // Keyboard Events 'on:keydown'?: KeyboardEventHandler | undefined | null; @@ -727,6 +731,7 @@ export interface HTMLAttributes extends AriaAttributes, D title?: string | undefined | null; translate?: 'yes' | 'no' | '' | undefined | null; inert?: boolean | undefined | null; + popover?: 'auto' | 'manual' | '' | undefined | null; // Unknown radiogroup?: string | undefined | null; // , @@ -873,6 +878,8 @@ export interface HTMLButtonAttributes extends HTMLAttributes name?: string | undefined | null; type?: 'submit' | 'reset' | 'button' | undefined | null; value?: string | string[] | number | undefined | null; + popovertarget?: string | undefined | null; + popovertargetaction?: 'toggle' | 'show' | 'hide' | undefined | null; } export interface HTMLCanvasAttributes extends HTMLAttributes { @@ -897,6 +904,10 @@ export interface HTMLDetailsAttributes extends HTMLAttributes | undefined | null; + ontoggle?: EventHandler | undefined | null; + ontogglecapture?: EventHandler | undefined | null; } export interface HTMLDelAttributes extends HTMLAttributes { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 50fb4f09c8..a1b62c22cf 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.0.0-next.27", + "version": "5.0.0-next.29", "type": "module", "types": "./types/index.d.ts", "engines": { @@ -90,11 +90,12 @@ "templating" ], "scripts": { - "build": "rollup -c && node scripts/build.js && node scripts/check-treeshakeability.js", + "build": "rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js", "dev": "rollup -cw", "check": "tsc && cd ./tests/types && tsc", "check:watch": "tsc --watch", "generate:version": "node ./scripts/generate-version.js", + "generate:types": "node ./scripts/generate-types.js", "prepublishOnly": "pnpm build" }, "devDependencies": { @@ -106,7 +107,7 @@ "@rollup/plugin-virtual": "^3.0.2", "@types/aria-query": "^5.0.3", "@types/estree": "^1.0.5", - "dts-buddy": "^0.4.0", + "dts-buddy": "^0.4.3", "esbuild": "^0.19.2", "rollup": "^4.1.5", "source-map": "^0.7.4", @@ -116,7 +117,7 @@ "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "acorn": "^8.10.0", - "acorn-typescript": "^1.4.11", + "acorn-typescript": "^1.4.13", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "esm-env": "^1.0.0", diff --git a/packages/svelte/scripts/build.js b/packages/svelte/scripts/generate-types.js similarity index 100% rename from packages/svelte/scripts/build.js rename to packages/svelte/scripts/generate-types.js diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index ad23d47d6b..23fa756a9b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -2,7 +2,7 @@ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; import { tsPlugin } from 'acorn-typescript'; -const ParserWithTS = acorn.Parser.extend(tsPlugin()); +const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true })); /** * @param {string} source diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 6b7642c251..e99e91d052 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -11,7 +11,7 @@ import read_options from './read/options.js'; const regex_position_indicator = / \(\d+:\d+\)$/; const regex_lang_attribute = - /|]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/; + /|]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g; export class Parser { /** @@ -49,7 +49,14 @@ export class Parser { this.template = template.trimRight(); - this.ts = regex_lang_attribute.exec(template)?.[2] === 'ts'; + let match_lang; + + do match_lang = regex_lang_attribute.exec(template); + while (match_lang && match_lang[0][1] !== 's'); // ensure it starts with '|\|\|)\s*/; const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/; const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/; -const REGEX_NTH_OF = /^\s*(even|odd|(-?[0-9]?n?(\s*\+\s*[0-9]+)?))(\s*(?=[,)])|\s+of\s+)/; +const REGEX_NTH_OF = + /^(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/; const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_BRACE_OR_SEMICOLON = /[{;]/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/; @@ -152,6 +153,8 @@ function read_selector_list(parser, inside_pseudo_class = false) { /** @type {import('#compiler').Css.Selector[]} */ const children = []; + allow_comment_or_whitespace(parser); + const start = parser.index; while (parser.index < parser.template.length) { @@ -226,6 +229,12 @@ function read_selector(parser, inside_pseudo_class = false) { start, end: parser.index }); + // We read the inner selectors of a pseudo element to ensure it parses correctly, + // but we don't do anything with the result. + if (parser.eat('(')) { + read_selector_list(parser, true); + parser.eat(')', true); + } } else if (parser.eat(':')) { const name = read_identifier(parser); @@ -277,6 +286,15 @@ function read_selector(parser, inside_pseudo_class = false) { value, flags }); + } else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) { + // nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator + + children.push({ + type: 'Nth', + value: /**@type {string} */ (parser.read(REGEX_NTH_OF)), + start, + end: parser.index + }); } else if (parser.match_regex(REGEX_COMBINATOR_WHITESPACE)) { parser.allow_whitespace(); const start = parser.index; @@ -294,13 +312,6 @@ function read_selector(parser, inside_pseudo_class = false) { start, end: parser.index }); - } else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) { - children.push({ - type: 'Nth', - value: /** @type {string} */ (parser.read(REGEX_NTH_OF)), - start, - end: parser.index - }); } else { let name = read_identifier(parser); if (parser.match('|')) { diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/html.js b/packages/svelte/src/compiler/phases/1-parse/utils/html.js index bb45f9d6db..309569f4b7 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/html.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/html.js @@ -121,15 +121,8 @@ function validate_code(code) { // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission -const interactive_elements = new Set([ - 'a', - 'button', - 'iframe', - 'embed', - 'input', - 'select', - 'textarea' -]); +// while `input` is also an interactive element, it is never moved by the browser, so we don't need to check for it +const interactive_elements = new Set(['a', 'button', 'iframe', 'embed', 'select', 'textarea']); /** @type {Record>} */ const disallowed_contents = { diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js index 2a8e4eff40..b6796ff961 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -184,7 +184,9 @@ export default class Selector { selector.name === 'global' && block.selectors.length !== 1 && (i === block.selectors.length - 1 || - block.selectors.slice(i + 1).some((s) => s.type !== 'PseudoElementSelector')) + block.selectors + .slice(i + 1) + .some((s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector')) ) { error(selector, 'invalid-css-global-selector-list'); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index e7f9e1a727..dc98fb31e6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -183,6 +183,10 @@ function get_delegated_event(node, context) { ) { return non_hoistable; } + // If we referebnce the index within an each block, then bail-out. + if (binding !== null && binding.initial?.type === 'EachBlock') { + return non_hoistable; + } if ( binding !== null && diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index b2928105e1..7b7076a69c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -525,6 +525,8 @@ export function should_proxy_or_freeze(node) { node.type === 'Literal' || node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression' || + node.type === 'UnaryExpression' || + node.type === 'BinaryExpression' || (node.type === 'Identifier' && node.name === 'undefined') ) { return false; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index b2ae9e086e..66f9a54ebf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -289,7 +289,7 @@ function setup_select_synchronization(value_binding, context) { * value = $.spread_attributes(element, value, [...]) * }); * ``` - * Returns the id of the spread_attribute variable if spread is deemed reactive, `null` otherwise. + * Returns the id of the spread_attribute variable if spread isn't isolated, `null` otherwise. * @param {Array} attributes * @param {import('../types.js').ComponentContext} context * @param {import('#compiler').RegularElement} element @@ -297,7 +297,7 @@ function setup_select_synchronization(value_binding, context) { * @returns {string | null} */ function serialize_element_spread_attributes(attributes, context, element, element_id) { - let is_reactive = false; + let needs_isolation = false; /** @type {import('estree').Expression[]} */ const values = []; @@ -312,18 +312,32 @@ function serialize_element_spread_attributes(attributes, context, element, eleme values.push(/** @type {import('estree').Expression} */ (context.visit(attribute))); } - is_reactive ||= - attribute.metadata.dynamic || - (attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression); + needs_isolation ||= + attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression; } const lowercase_attributes = element.metadata.svg || is_custom_element_node(element) ? b.false : b.true; - if (is_reactive) { + const isolated = b.stmt( + b.call( + '$.spread_attributes_effect', + element_id, + b.thunk(b.array(values)), + lowercase_attributes, + b.literal(context.state.analysis.stylesheet.id) + ) + ); + + // objects could contain reactive getters -> play it safe and always assume spread attributes are reactive + if (needs_isolation) { + context.state.update_effects.push(isolated); + return null; + } else { const id = context.state.scope.generate('spread_attributes'); - context.state.init.push(b.let(id, undefined)); + context.state.init.push(b.let(id)); context.state.update.push({ + singular: isolated, grouped: b.stmt( b.assignment( '=', @@ -340,20 +354,6 @@ function serialize_element_spread_attributes(attributes, context, element, eleme ) }); return id; - } else { - context.state.init.push( - b.stmt( - b.call( - '$.spread_attributes', - element_id, - b.literal(null), - b.array(values), - lowercase_attributes, - b.literal(context.state.analysis.stylesheet.id) - ) - ) - ); - return null; } } @@ -365,7 +365,7 @@ function serialize_element_spread_attributes(attributes, context, element, eleme * @param {import('estree').Identifier} element_id * @returns {boolean} */ -function serialize_dynamic_element_spread_attributes(attributes, context, element_id) { +function serialize_dynamic_element_attributes(attributes, context, element_id) { if (attributes.length === 0) { if (context.state.analysis.stylesheet.id) { context.state.init.push( @@ -375,6 +375,7 @@ function serialize_dynamic_element_spread_attributes(attributes, context, elemen return false; } + let needs_isolation = false; let is_reactive = false; /** @type {import('estree').Expression[]} */ @@ -388,13 +389,31 @@ function serialize_dynamic_element_spread_attributes(attributes, context, elemen values.push(/** @type {import('estree').Expression} */ (context.visit(attribute))); } - is_reactive ||= attribute.metadata.dynamic; + is_reactive ||= + attribute.metadata.dynamic || + // objects could contain reactive getters -> play it safe and always assume spread attributes are reactive + attribute.type === 'SpreadAttribute'; + needs_isolation ||= + attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression; } - if (is_reactive) { + const isolated = b.stmt( + b.call( + '$.spread_dynamic_element_attributes_effect', + element_id, + b.thunk(b.array(values)), + b.literal(context.state.analysis.stylesheet.id) + ) + ); + + if (needs_isolation) { + context.state.update_effects.push(isolated); + return false; + } else if (is_reactive) { const id = context.state.scope.generate('spread_attributes'); context.state.init.push(b.let(id)); context.state.update.push({ + singular: isolated, grouped: b.stmt( b.assignment( '=', @@ -1506,6 +1525,7 @@ function process_children(nodes, parent, { visit, state }) { expression = b.call('$.sibling', text_id); } + let is_fragment = false; for (let i = 0; i < nodes.length; i += 1) { const node = nodes[i]; @@ -1513,7 +1533,12 @@ function process_children(nodes, parent, { visit, state }) { sequence.push(node); } else { if (sequence.length > 0) { - flush_sequence(sequence, true); + flush_sequence(sequence, is_fragment); + // Ensure we move to the next sibling for the case where we move reference within a fragment + if (!is_fragment && sequence.length === 1 && sequence[0].type === 'ExpressionTag') { + expression = b.call('$.sibling', expression); + is_fragment = true; + } sequence = []; } @@ -2105,7 +2130,7 @@ export const template_visitors = { // Always use spread because we don't know whether the element is a custom element or not, // therefore we need to do the "how to set an attribute" logic at runtime. const is_attributes_reactive = - serialize_dynamic_element_spread_attributes(attributes, inner_context, element_id) !== null; + serialize_dynamic_element_attributes(attributes, inner_context, element_id) !== null; // class/style directives must be applied last since they could override class/style attributes serialize_class_directives(class_directives, element_id, inner_context, is_attributes_reactive); diff --git a/packages/svelte/src/compiler/phases/3-transform/typescript.js b/packages/svelte/src/compiler/phases/3-transform/typescript.js index 9449ced6a0..e34d75691e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/typescript.js +++ b/packages/svelte/src/compiler/phases/3-transform/typescript.js @@ -41,13 +41,16 @@ export const remove_types = { TSAsExpression(node, context) { return context.visit(node.expression); }, + TSSatisfiesExpression(node, context) { + return context.visit(node.expression); + }, TSNonNullExpression(node, context) { return context.visit(node.expression); }, - TSInterfaceDeclaration(node, context) { + TSInterfaceDeclaration() { return b.empty; }, - TSTypeAliasDeclaration(node, context) { + TSTypeAliasDeclaration() { return b.empty; } }; diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 0d0bbffe35..02f111697d 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -65,7 +65,7 @@ export class Scope { * @param {import('estree').Identifier} node * @param {import('#compiler').Binding['kind']} kind * @param {import('#compiler').DeclarationKind} declaration_kind - * @param {null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration} initial + * @param {null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration | import('../types/template.js').EachBlock} initial * @returns {import('#compiler').Binding} */ declare(node, kind, declaration_kind, initial = null) { @@ -523,7 +523,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const is_keyed = node.key && (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index); - scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const'); + scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const', node); } if (node.key) visit(node.key, { scope }); @@ -678,7 +678,7 @@ export function set_scope(scopes) { /** * Returns the name of the rune if the given expression is a `CallExpression` using a rune. - * @param {import('estree').Node | null | undefined} node + * @param {import('estree').Node | import('../types/template.js').EachBlock | null | undefined} node * @param {Scope} scope * @returns {Runes[number] | null} */ diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index fcb960847e..b62b47bf0d 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -11,7 +11,7 @@ import type { SourceMap } from 'magic-string'; import type { Context } from 'zimmerframe'; import type { Scope } from '../phases/scope.js'; import * as Css from './css.js'; -import type { Namespace, SvelteNode } from './template.js'; +import type { EachBlock, Namespace, SvelteNode } from './template.js'; /** The return value of `compile` from `svelte/compiler` */ export interface CompileResult { @@ -269,7 +269,13 @@ export interface Binding { * What the value was initialized with. * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` */ - initial: null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration; + initial: + | null + | Expression + | FunctionDeclaration + | ClassDeclaration + | ImportDeclaration + | EachBlock; is_called: boolean; references: { node: Identifier; path: SvelteNode[] }[]; mutated: boolean; diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 3df03ded08..e1f211e902 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -1,6 +1,7 @@ export const EACH_ITEM_REACTIVE = 1; export const EACH_INDEX_REACTIVE = 1 << 1; export const EACH_KEYED = 1 << 2; +export const EACH_PROXIED = 1 << 3; export const EACH_IS_CONTROLLED = 1 << 3; export const EACH_IS_ANIMATED = 1 << 4; export const EACH_IS_IMMUTABLE = 1 << 6; diff --git a/packages/svelte/src/internal/client/each.js b/packages/svelte/src/internal/client/each.js index e493241fed..8d9ef0e7c7 100644 --- a/packages/svelte/src/internal/client/each.js +++ b/packages/svelte/src/internal/client/each.js @@ -351,13 +351,8 @@ function reconcile_tracked_array( ) { var a_blocks = each_block.v; const is_computed_key = keys !== null; - var is_proxied_array = STATE_SYMBOL in array && /** @type {any} */ (array[STATE_SYMBOL]).i; var active_transitions = each_block.s; - if (is_proxied_array) { - flags &= ~EACH_ITEM_REACTIVE; - } - /** @type {number | void} */ var a = a_blocks.length; diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 4aeb067f98..d6cf78e5f5 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -1280,6 +1280,10 @@ function handle_event_propagation(root_element, event) { const handled_at = event.__root; if (handled_at) { const at_idx = path.indexOf(handled_at); + if (at_idx !== -1 && root_element === document) { + // This is the fallback document listener but the event was already handled -> ignore + return; + } if (at_idx < path.indexOf(root_element)) { path_idx = at_idx; } @@ -2424,10 +2428,26 @@ function get_setters(element) { return setters; } +/** + * Like `spread_attributes` but self-contained + * @param {Element & ElementCSSInlineStyle} dom + * @param {() => Record[]} attrs + * @param {boolean} lowercase_attributes + * @param {string} css_hash + */ +export function spread_attributes_effect(dom, attrs, lowercase_attributes, css_hash) { + /** @type {Record | undefined} */ + let current = undefined; + + render_effect(() => { + current = spread_attributes(dom, current, attrs(), lowercase_attributes, css_hash); + }); +} + /** * Spreads attributes onto a DOM element, taking into account the currently set attributes * @param {Element & ElementCSSInlineStyle} dom - * @param {Record | null} prev + * @param {Record | undefined} prev * @param {Record[]} attrs * @param {boolean} lowercase_attributes * @param {string} css_hash @@ -2524,18 +2544,30 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha /** * @param {Element} node - * @param {Record | null} prev + * @param {() => Record[]} attrs + * @param {string} css_hash + */ +export function spread_dynamic_element_attributes_effect(node, attrs, css_hash) { + /** @type {Record | undefined} */ + let current = undefined; + + render_effect(() => { + current = spread_dynamic_element_attributes(node, current, attrs(), css_hash); + }); +} + +/** + * @param {Element} node + * @param {Record | undefined} prev * @param {Record[]} attrs * @param {string} css_hash */ export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) { if (node.tagName.includes('-')) { const next = object_assign({}, ...attrs); - if (prev !== null) { - for (const key in prev) { - if (!(key in next)) { - next[key] = null; - } + for (const key in prev) { + if (!(key in next)) { + next[key] = null; } } for (const key in next) { @@ -2778,6 +2810,7 @@ export function mount(component, options) { set_current_hydration_fragment(previous_hydration_fragment); } const bound_event_listener = handle_event_propagation.bind(null, container); + const bound_document_event_listener = handle_event_propagation.bind(null, document); /** @param {Array} events */ const event_handle = (events) => { @@ -2785,6 +2818,9 @@ export function mount(component, options) { const event_name = events[i]; if (!registered_events.has(event_name)) { registered_events.add(event_name); + // Add the event listener to both the container and the document. + // The container listener ensures we catch events from within in case + // the outer content stops propagation of the event. container.addEventListener( event_name, bound_event_listener, @@ -2794,6 +2830,17 @@ export function mount(component, options) { } : undefined ); + // The document listener ensures we catch events that originate from elements that were + // manually moved outside of the container (e.g. via manual portals). + document.addEventListener( + event_name, + bound_document_event_listener, + PassiveDelegatedEvents.includes(event_name) + ? { + passive: true + } + : undefined + ); } } }; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3659a315ae..a95253c731 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -64,7 +64,7 @@ let current_dependencies = null; let current_dependencies_index = 0; /** @type {null | import('./types.js').Signal[]} */ let current_untracked_writes = null; -/** @type {null | import('./types.js').Signal} */ +/** @type {null | import('./types.js').SignalDebug} */ let last_inspected_signal = null; /** If `true`, `get`ting the signal should not register it as a dependency */ export let current_untracking = false; @@ -81,7 +81,7 @@ let captured_signals = new Set(); /** @type {Function | null} */ let inspect_fn = null; -/** @type {Array} */ +/** @type {Array} */ let inspect_captured_signals = []; // Handle rendering tree blocks and anchors @@ -127,7 +127,6 @@ export function batch_inspect(target, prop, receiver) { } finally { is_batching_effect = previously_batching_effect; if (last_inspected_signal !== null) { - // @ts-expect-error for (const fn of last_inspected_signal.inspect) fn(); last_inspected_signal = null; } @@ -349,7 +348,21 @@ function execute_signal_fn(signal) { if (current_dependencies !== null) { let i; - remove_consumer(signal, current_dependencies_index, false); + if (dependencies !== null) { + const dep_length = dependencies.length; + // If we have more than 16 elements in the array then use a Set for faster performance + // TODO: evaluate if we should always just use a Set or not here? + const current_dependencies_set = dep_length > 16 ? new Set(current_dependencies) : null; + for (i = current_dependencies_index; i < dep_length; i++) { + const dependency = dependencies[i]; + if ( + (current_dependencies_set !== null && !current_dependencies_set.has(dependency)) || + !current_dependencies.includes(dependency) + ) { + remove_consumer(signal, dependency, false); + } + } + } if (dependencies !== null && current_dependencies_index > 0) { dependencies.length = current_dependencies_index + current_dependencies.length; @@ -365,16 +378,17 @@ function execute_signal_fn(signal) { if (!current_skip_consumer) { for (i = current_dependencies_index; i < dependencies.length; i++) { const dependency = dependencies[i]; + const consumers = dependency.c; - if (dependency.c === null) { + if (consumers === null) { dependency.c = [signal]; - } else { - dependency.c.push(signal); + } else if (consumers[consumers.length - 1] !== signal) { + consumers.push(signal); } } } } else if (dependencies !== null && current_dependencies_index < dependencies.length) { - remove_consumer(signal, current_dependencies_index, false); + remove_consumers(signal, current_dependencies_index, false); dependencies.length = current_dependencies_index; } return res; @@ -390,6 +404,40 @@ function execute_signal_fn(signal) { } } +/** + * @template V + * @param {import('./types.js').ComputationSignal} signal + * @param {import('./types.js').Signal} dependency + * @param {boolean} remove_unowned + * @returns {void} + */ +function remove_consumer(signal, dependency, remove_unowned) { + const consumers = dependency.c; + let consumers_length = 0; + if (consumers !== null) { + consumers_length = consumers.length - 1; + const index = consumers.indexOf(signal); + if (index !== -1) { + if (consumers_length === 0) { + dependency.c = null; + } else { + // Swap with last element and then remove. + consumers[index] = consumers[consumers_length]; + consumers.pop(); + } + } + } + if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) { + // If the signal is unowned then we need to make sure to change it to dirty. + set_signal_status(dependency, DIRTY); + remove_consumers( + /** @type {import('./types.js').ComputationSignal} **/ (dependency), + 0, + true + ); + } +} + /** * @template V * @param {import('./types.js').ComputationSignal} signal @@ -397,36 +445,13 @@ function execute_signal_fn(signal) { * @param {boolean} remove_unowned * @returns {void} */ -function remove_consumer(signal, start_index, remove_unowned) { +function remove_consumers(signal, start_index, remove_unowned) { const dependencies = signal.d; if (dependencies !== null) { let i; for (i = start_index; i < dependencies.length; i++) { const dependency = dependencies[i]; - const consumers = dependency.c; - let consumers_length = 0; - if (consumers !== null) { - consumers_length = consumers.length - 1; - const index = consumers.indexOf(signal); - if (index !== -1) { - if (consumers_length === 0) { - dependency.c = null; - } else { - // Swap with last element and then remove. - consumers[index] = consumers[consumers_length]; - consumers.pop(); - } - } - } - if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) { - // If the signal is unowned then we need to make sure to change it to dirty. - set_signal_status(dependency, DIRTY); - remove_consumer( - /** @type {import('./types.js').ComputationSignal} **/ (dependency), - 0, - true - ); - } + remove_consumer(signal, dependency, remove_unowned); } } } @@ -446,7 +471,7 @@ function destroy_references(signal) { if ((reference.f & IS_EFFECT) !== 0) { destroy_signal(reference); } else { - remove_consumer(reference, 0, true); + remove_consumers(reference, 0, true); reference.d = null; } } @@ -711,8 +736,7 @@ function update_derived(signal, force_schedule) { // @ts-expect-error if (DEV && signal.inspect && force_schedule) { - // @ts-expect-error - for (const fn of signal.inspect) fn(); + for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn(); } } } @@ -815,8 +839,7 @@ export function unsubscribe_on_destroy(stores) { export function get(signal) { // @ts-expect-error if (DEV && signal.inspect && inspect_fn) { - // @ts-expect-error - signal.inspect.add(inspect_fn); + /** @type {import('./types.js').SignalDebug} */ (signal).inspect.add(inspect_fn); // @ts-expect-error inspect_captured_signals.push(signal); } @@ -841,10 +864,16 @@ export function get(signal) { !(unowned && current_effect !== null) ) { current_dependencies_index++; - } else if (current_dependencies === null) { - current_dependencies = [signal]; - } else if (signal !== current_dependencies[current_dependencies.length - 1]) { - current_dependencies.push(signal); + } else if ( + dependencies === null || + current_dependencies_index === 0 || + dependencies[current_dependencies_index - 1] !== signal + ) { + if (current_dependencies === null) { + current_dependencies = [signal]; + } else if (signal !== current_dependencies[current_dependencies.length - 1]) { + current_dependencies.push(signal); + } } if ( current_untracked_writes !== null && @@ -1079,10 +1108,9 @@ export function set_signal_value(signal, value) { // @ts-expect-error if (DEV && signal.inspect) { if (is_batching_effect) { - last_inspected_signal = signal; + last_inspected_signal = /** @type {import('./types.js').SignalDebug} */ (signal); } else { - // @ts-expect-error - for (const fn of signal.inspect) fn(); + for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn(); } } } @@ -1098,7 +1126,7 @@ export function destroy_signal(signal) { const destroy = signal.y; const flags = signal.f; destroy_references(signal); - remove_consumer(signal, 0, true); + remove_consumers(signal, 0, true); signal.i = signal.r = signal.y = @@ -1804,6 +1832,37 @@ function deep_read(value, visited = new Set()) { } } +/** + * Like `unstate`, but recursively traverses into normal arrays/objects to find potential states in them. + * @param {any} value + * @param {Map} visited + * @returns {any} + */ +function deep_unstate(value, visited = new Map()) { + if (typeof value === 'object' && value !== null && !visited.has(value)) { + const unstated = unstate(value); + if (unstated !== value) { + visited.set(value, unstated); + return unstated; + } + + let contains_unstated = false; + /** @type {any} */ + const nested_unstated = Array.isArray(value) ? [] : {}; + for (let key in value) { + const result = deep_unstate(value[key], visited); + nested_unstated[key] = result; + if (result !== value[key]) { + contains_unstated = true; + } + } + + visited.set(value, contains_unstated ? nested_unstated : value); + } + + return visited.get(value) ?? value; +} + // TODO remove in a few versions, before 5.0 at the latest let warned_inspect_changed = false; @@ -1817,7 +1876,7 @@ export function inspect(get_value, inspect = console.log) { pre_effect(() => { const fn = () => { - const value = get_value().map(unstate); + const value = get_value().map((v) => deep_unstate(v)); if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) { // eslint-disable-next-line no-console console.warn( diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 3a73f797e3..af21817c69 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -108,6 +108,8 @@ export type ComputationSignal = { export type Signal = SourceSignal | ComputationSignal; +export type SignalDebug = SourceSignalDebug & Signal; + export type EffectSignal = ComputationSignal void)>; export type MaybeSignal = T | Signal; diff --git a/packages/svelte/src/main/main-server.js b/packages/svelte/src/main/main-server.js index b9f81c5253..844aec50d1 100644 --- a/packages/svelte/src/main/main-server.js +++ b/packages/svelte/src/main/main-server.js @@ -20,3 +20,13 @@ export function beforeUpdate() {} /** @returns {void} */ export function afterUpdate() {} + +/** + * @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; +} diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index ab3307900a..17b4be4464 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.27'; +export const VERSION = '5.0.0-next.29'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/parser-modern/samples/comment-before-script/input.svelte b/packages/svelte/tests/parser-modern/samples/comment-before-script/input.svelte new file mode 100644 index 0000000000..5086ed08bb --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/comment-before-script/input.svelte @@ -0,0 +1,4 @@ + + diff --git a/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json b/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json new file mode 100644 index 0000000000..d52a3752b4 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json @@ -0,0 +1,132 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 27, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Comment", + "start": 0, + "end": 27, + "data": "should not error out", + "ignores": [] + }, + { + "type": "Text", + "start": 27, + "end": 28, + "raw": "\n", + "data": "\n" + } + ], + "transparent": false + }, + "options": null, + "instance": { + "type": "Script", + "start": 28, + "end": 76, + "context": "default", + "content": { + "type": "Program", + "start": 46, + "end": 67, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 4, + "column": 0 + } + }, + "body": [ + { + "type": "VariableDeclaration", + "start": 48, + "end": 66, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 19 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 52, + "end": 65, + "loc": { + "start": { + "line": 3, + "column": 5 + }, + "end": { + "line": 3, + "column": 18 + } + }, + "id": { + "type": "Identifier", + "start": 52, + "end": 18, + "loc": { + "start": { + "line": 3, + "column": 5 + }, + "end": { + "line": 3, + "column": 18 + } + }, + "name": "count", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 57, + "end": 65, + "loc": { + "start": { + "line": 3, + "column": 10 + }, + "end": { + "line": 3, + "column": 18 + } + }, + "typeAnnotation": { + "type": "TSNumberKeyword", + "start": 59, + "end": 65, + "loc": { + "start": { + "line": 3, + "column": 12 + }, + "end": { + "line": 3, + "column": 18 + } + } + } + } + }, + "init": null + } + ], + "kind": "let" + } + ], + "sourceType": "module" + } + } +} diff --git a/packages/svelte/tests/parser-modern/samples/css-nth-syntax/input.svelte b/packages/svelte/tests/parser-modern/samples/css-nth-syntax/input.svelte index 5668a7799a..ce3813f3ad 100644 --- a/packages/svelte/tests/parser-modern/samples/css-nth-syntax/input.svelte +++ b/packages/svelte/tests/parser-modern/samples/css-nth-syntax/input.svelte @@ -28,6 +28,18 @@ } h1:global(nav) { background: red; + } + h1:nth-of-type(10n+1){ + background: red; + } + h1:nth-of-type(-2n+3){ + background: red; + } + h1:nth-of-type(+12){ + background: red; + } + h1:nth-of-type(+3n){ + background: red; } diff --git a/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json b/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json index 3716981b37..9e13a7cb86 100644 --- a/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json +++ b/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json @@ -2,7 +2,7 @@ "css": { "type": "Style", "start": 0, - "end": 586, + "end": 806, "attributes": [], "children": [ { @@ -495,19 +495,19 @@ "name": "nth-child", "args": { "type": "SelectorList", - "start": 476, - "end": 491, + "start": 485, + "end": 486, "children": [ { "type": "Selector", - "start": 476, - "end": 491, + "start": 485, + "end": 486, "children": [ { "type": "Nth", - "value": "\n n\n ", - "start": 476, - "end": 491 + "value": "n", + "start": 485, + "end": 486 } ] } @@ -601,32 +601,292 @@ }, "start": 530, "end": 577 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 580, + "end": 601, + "children": [ + { + "type": "Selector", + "start": 580, + "end": 601, + "children": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 580, + "end": 582 + }, + { + "type": "PseudoClassSelector", + "name": "nth-of-type", + "args": { + "type": "SelectorList", + "start": 595, + "end": 600, + "children": [ + { + "type": "Selector", + "start": 595, + "end": 600, + "children": [ + { + "type": "Nth", + "value": "10n+1", + "start": 595, + "end": 600 + } + ] + } + ] + }, + "start": 582, + "end": 601 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 601, + "end": 633, + "children": [ + { + "type": "Declaration", + "start": 611, + "end": 626, + "property": "background", + "value": "red" + } + ] + }, + "start": 580, + "end": 633 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 636, + "end": 657, + "children": [ + { + "type": "Selector", + "start": 636, + "end": 657, + "children": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 636, + "end": 638 + }, + { + "type": "PseudoClassSelector", + "name": "nth-of-type", + "args": { + "type": "SelectorList", + "start": 651, + "end": 656, + "children": [ + { + "type": "Selector", + "start": 651, + "end": 656, + "children": [ + { + "type": "Nth", + "value": "-2n+3", + "start": 651, + "end": 656 + } + ] + } + ] + }, + "start": 638, + "end": 657 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 657, + "end": 689, + "children": [ + { + "type": "Declaration", + "start": 667, + "end": 682, + "property": "background", + "value": "red" + } + ] + }, + "start": 636, + "end": 689 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 692, + "end": 711, + "children": [ + { + "type": "Selector", + "start": 692, + "end": 711, + "children": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 692, + "end": 694 + }, + { + "type": "PseudoClassSelector", + "name": "nth-of-type", + "args": { + "type": "SelectorList", + "start": 707, + "end": 710, + "children": [ + { + "type": "Selector", + "start": 707, + "end": 710, + "children": [ + { + "type": "Nth", + "value": "+12", + "start": 707, + "end": 710 + } + ] + } + ] + }, + "start": 694, + "end": 711 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 711, + "end": 743, + "children": [ + { + "type": "Declaration", + "start": 721, + "end": 736, + "property": "background", + "value": "red" + } + ] + }, + "start": 692, + "end": 743 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 746, + "end": 765, + "children": [ + { + "type": "Selector", + "start": 746, + "end": 765, + "children": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 746, + "end": 748 + }, + { + "type": "PseudoClassSelector", + "name": "nth-of-type", + "args": { + "type": "SelectorList", + "start": 761, + "end": 764, + "children": [ + { + "type": "Selector", + "start": 761, + "end": 764, + "children": [ + { + "type": "Nth", + "value": "+3n", + "start": 761, + "end": 764 + } + ] + } + ] + }, + "start": 748, + "end": 765 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 765, + "end": 797, + "children": [ + { + "type": "Declaration", + "start": 775, + "end": 790, + "property": "background", + "value": "red" + } + ] + }, + "start": 746, + "end": 797 } ], "content": { "start": 7, - "end": 578, - "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" + "end": 798, + "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" } }, "js": [], - "start": 588, - "end": 600, + "start": 808, + "end": 820, "type": "Root", "fragment": { "type": "Fragment", "nodes": [ { "type": "Text", - "start": 586, - "end": 588, + "start": 806, + "end": 808, "raw": "\n\n", "data": "\n\n" }, { "type": "RegularElement", - "start": 588, - "end": 600, + "start": 808, + "end": 820, "name": "h1", "attributes": [], "fragment": { @@ -634,8 +894,8 @@ "nodes": [ { "type": "Text", - "start": 592, - "end": 595, + "start": 812, + "end": 815, "raw": "Foo", "data": "Foo" } diff --git a/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/input.svelte b/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/input.svelte new file mode 100644 index 0000000000..d8ed0f9e36 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/input.svelte @@ -0,0 +1,24 @@ + diff --git a/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json b/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json new file mode 100644 index 0000000000..1431b123a4 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json @@ -0,0 +1,330 @@ +{ + "css": { + "type": "Style", + "start": 0, + "end": 386, + "attributes": [], + "children": [ + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 60, + "end": 86, + "children": [ + { + "type": "Selector", + "start": 60, + "end": 86, + "children": [ + { + "type": "PseudoElementSelector", + "name": "view-transition-old", + "start": 60, + "end": 81 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 88, + "end": 109, + "children": [ + { + "type": "Declaration", + "start": 92, + "end": 102, + "property": "color", + "value": "red" + } + ] + }, + "start": 60, + "end": 109 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 111, + "end": 146, + "children": [ + { + "type": "Selector", + "start": 111, + "end": 146, + "children": [ + { + "type": "PseudoClassSelector", + "name": "global", + "args": { + "type": "SelectorList", + "start": 119, + "end": 145, + "children": [ + { + "type": "Selector", + "start": 119, + "end": 145, + "children": [ + { + "type": "PseudoElementSelector", + "name": "view-transition-old", + "start": 119, + "end": 140 + } + ] + } + ] + }, + "start": 111, + "end": 146 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 148, + "end": 169, + "children": [ + { + "type": "Declaration", + "start": 152, + "end": 162, + "property": "color", + "value": "red" + } + ] + }, + "start": 111, + "end": 169 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 171, + "end": 199, + "children": [ + { + "type": "Selector", + "start": 171, + "end": 199, + "children": [ + { + "type": "PseudoElementSelector", + "name": "highlight", + "start": 171, + "end": 182 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 200, + "end": 218, + "children": [ + { + "type": "Declaration", + "start": 204, + "end": 214, + "property": "color", + "value": "red" + } + ] + }, + "start": 171, + "end": 218 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 220, + "end": 245, + "children": [ + { + "type": "Selector", + "start": 220, + "end": 245, + "children": [ + { + "type": "TypeSelector", + "name": "custom-element", + "start": 220, + "end": 234 + }, + { + "type": "PseudoElementSelector", + "name": "part", + "start": 234, + "end": 240 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 246, + "end": 264, + "children": [ + { + "type": "Declaration", + "start": 250, + "end": 260, + "property": "color", + "value": "red" + } + ] + }, + "start": 220, + "end": 264 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 266, + "end": 285, + "children": [ + { + "type": "Selector", + "start": 266, + "end": 285, + "children": [ + { + "type": "PseudoElementSelector", + "name": "slotted", + "start": 266, + "end": 275 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 286, + "end": 304, + "children": [ + { + "type": "Declaration", + "start": 290, + "end": 300, + "property": "color", + "value": "red" + } + ] + }, + "start": 266, + "end": 304 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 306, + "end": 359, + "children": [ + { + "type": "Selector", + "start": 306, + "end": 359, + "children": [ + { + "type": "PseudoClassSelector", + "name": "is", + "args": { + "type": "SelectorList", + "start": 324, + "end": 355, + "children": [ + { + "type": "Selector", + "start": 324, + "end": 330, + "children": [ + { + "type": "TypeSelector", + "name": "button", + "start": 324, + "end": 330 + } + ] + }, + { + "type": "Selector", + "start": 349, + "end": 355, + "children": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 349, + "end": 351 + }, + { + "type": "Combinator", + "name": "+", + "start": 352, + "end": 353 + }, + { + "type": "TypeSelector", + "name": "p", + "start": 354, + "end": 355 + } + ] + } + ] + }, + "start": 306, + "end": 359 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 359, + "end": 377, + "children": [ + { + "type": "Declaration", + "start": 363, + "end": 373, + "property": "color", + "value": "red" + } + ] + }, + "start": 306, + "end": 377 + } + ], + "content": { + "start": 7, + "end": 378, + "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" + } + }, + "js": [], + "start": null, + "end": null, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [], + "transparent": false + }, + "options": null +} diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-call-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/attribute-spread-call-expression/_config.js new file mode 100644 index 0000000000..8b25b7ad43 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-call-expression/_config.js @@ -0,0 +1,62 @@ +import { test } from '../../test'; + +export default test({ + html: ` + + + + + `, + + async test({ assert, target }) { + const [b1, b2, b3, b4] = target.querySelectorAll('button'); + + b1?.click(); + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + ` + ); + + b2?.click(); + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + ` + ); + + b3?.click(); + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + ` + ); + + b4?.click(); + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-call-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/attribute-spread-call-expression/main.svelte new file mode 100644 index 0000000000..4a239a7e09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-call-expression/main.svelte @@ -0,0 +1,24 @@ + + + + + +{values.c} +{values.d} diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-reactivitiy/_config.js b/packages/svelte/tests/runtime-runes/samples/attribute-spread-reactivitiy/_config.js new file mode 100644 index 0000000000..9baec30b4f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-reactivitiy/_config.js @@ -0,0 +1,24 @@ +import { test } from '../../test'; + +export default test({ + html: ` +
+
+
+
+ let value = $state('red'); + let tag = $state('div'); const getValue = () => { return value; @@ -10,9 +11,19 @@ const getSpread = () => { return { class: value }; } + const props = { + get class() { + return value; + } + }
- +
+ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/block-dependency-sequence/Component.svelte b/packages/svelte/tests/runtime-runes/samples/block-dependency-sequence/Component.svelte new file mode 100644 index 0000000000..ef8f82b73a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/block-dependency-sequence/Component.svelte @@ -0,0 +1,9 @@ + + +
+ {#if item} + {item.length} + {/if} +
diff --git a/packages/svelte/tests/runtime-runes/samples/block-dependency-sequence/_config.js b/packages/svelte/tests/runtime-runes/samples/block-dependency-sequence/_config.js new file mode 100644 index 0000000000..0d09ed54ec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/block-dependency-sequence/_config.js @@ -0,0 +1,26 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, component }) { + const [b1, b2] = target.querySelectorAll('button'); + assert.htmlEqual( + target.innerHTML, + '
5
5
3
+ diff --git a/packages/svelte/tests/runtime-runes/samples/each-text-template/_config.js b/packages/svelte/tests/runtime-runes/samples/each-text-template/_config.js new file mode 100644 index 0000000000..439863e4d9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-text-template/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

A
B
C

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-text-template/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-text-template/main.svelte new file mode 100644 index 0000000000..5d91831d0e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-text-template/main.svelte @@ -0,0 +1,9 @@ + + +

+{#each array as a} + {a}
+{/each} +

diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates/_config.js new file mode 100644 index 0000000000..a6cdafd20f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates/_config.js @@ -0,0 +1,39 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

test costs $1

test 2 costs $2

test costs $1

test 2 costs $2

`, + skip_if_ssr: 'permanent', + skip_if_hydrate: 'permanent', + + async test({ assert, target }) { + const [btn1, btn2, btn3] = target.querySelectorAll('button'); + + flushSync(() => { + btn2.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

test costs $1

test 2 costs $2000

test costs $1

test 2 costs $2000

` + ); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

test costs $1

test 2 costs $2000

test 3 costs $3

test costs $1

test 2 costs $2000

test 3 costs $3

` + ); + + flushSync(() => { + btn3.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

test costs $1

test 2 costs $2000

test costs $1

test 2 costs $2000

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates/main.svelte new file mode 100644 index 0000000000..731c47dde0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates/main.svelte @@ -0,0 +1,54 @@ + + +{#each items as item} +

{item.name} costs ${item.price}

+{/each} + +{#each items as item (item.id)} +

{item.name} costs ${item.price}

+{/each} + + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-call-expressions/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-call-expressions/_config.js deleted file mode 100644 index e9dae16277..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/event-attribute-call-expressions/_config.js +++ /dev/null @@ -1,16 +0,0 @@ -import { test } from '../../test'; - -export default test({ - html: `
' - ); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/_config.js b/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/_config.js new file mode 100644 index 0000000000..a4b60bd912 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/_config.js @@ -0,0 +1,23 @@ +import { test } from '../../test'; + +// Tests that event delegation still works when the element with the event listener is moved outside the container +export default test({ + async test({ assert, target }) { + const btn1 = target.parentElement?.querySelector('button'); + const btn2 = target.querySelector('button'); + + btn1?.click(); + await Promise.resolve(); + assert.htmlEqual( + target.parentElement?.innerHTML ?? '', + '
' + ); + + btn2?.click(); + await Promise.resolve(); + assert.htmlEqual( + target.parentElement?.innerHTML ?? '', + '
' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/main.svelte new file mode 100644 index 0000000000..47edcf7253 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/main.svelte @@ -0,0 +1,12 @@ + + +
+ + +
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js new file mode 100644 index 0000000000..32c9e98296 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js @@ -0,0 +1,40 @@ +import { test } from '../../test'; + +/** + * @type {any[]} + */ +let log; +/** + * @type {typeof console.log}} + */ +let original_log; + +export default test({ + compileOptions: { + dev: true + }, + before_test() { + log = []; + original_log = console.log; + console.log = (...v) => { + log.push(...v); + }; + }, + after_test() { + console.log = original_log; + }, + async test({ assert, target }) { + const [b1] = target.querySelectorAll('button'); + b1.click(); + await Promise.resolve(); + + assert.deepEqual(log, [ + 'init', + { x: { count: 0 } }, + [{ count: 0 }], + 'update', + { x: { count: 1 } }, + [{ count: 1 }] + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/main.svelte new file mode 100644 index 0000000000..68a171b80d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/main.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte index 724988b55a..e991aa7533 100644 --- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte @@ -10,6 +10,9 @@ import { type Bar, type Baz } from './types'; let count = $state(0); + const person = { + message: 'goodbye' + } satisfies Goodbye; diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/errors.json index 7ba32a53fb..53e24d8d81 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/errors.json @@ -3,11 +3,11 @@ "code": "invalid-css-global-placement", "message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", "start": { - "line": 2, + "line": 5, "column": 6 }, "end": { - "line": 2, + "line": 5, "column": 19 } } diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/input.svelte b/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/input.svelte index 75bd7b66e1..2c8e96a484 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/input.svelte +++ b/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/input.svelte @@ -1,4 +1,7 @@