From c2ec0d9ac29441ca387d74f0814dd27becf64369 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 24 Feb 2025 23:38:01 +0100 Subject: [PATCH 001/145] fix: allow for duplicate `var` declarations (#15382) --- .changeset/spotty-drinks-tan.md | 5 +++++ packages/svelte/src/compiler/phases/scope.js | 8 ++++++-- .../validator/samples/multiple-var-same-name/errors.json | 1 + .../validator/samples/multiple-var-same-name/input.svelte | 6 ++++++ 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 .changeset/spotty-drinks-tan.md create mode 100644 packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json create mode 100644 packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte diff --git a/.changeset/spotty-drinks-tan.md b/.changeset/spotty-drinks-tan.md new file mode 100644 index 0000000000..2150c8cffd --- /dev/null +++ b/.changeset/spotty-drinks-tan.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow for duplicate `var` declarations diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f46adf4900..7d9f90982a 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -161,8 +161,12 @@ export class Scope { } if (this.declarations.has(node.name)) { - // This also errors on var/function types, but that's arguably a good thing - e.declaration_duplicate(node, node.name); + const binding = this.declarations.get(node.name); + if (binding && binding.declaration_kind !== 'var' && declaration_kind !== 'var') { + // This also errors on function types, but that's arguably a good thing + // declaring function twice is also caught by acorn in the parse phase + e.declaration_duplicate(node, node.name); + } } const binding = new Binding(this, node, kind, declaration_kind, initial); diff --git a/packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json b/packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte b/packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte new file mode 100644 index 0000000000..19a8ef7722 --- /dev/null +++ b/packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte @@ -0,0 +1,6 @@ + + +{test} \ No newline at end of file From 3d59e84a65c855747ff17a226716ffa628d91aeb Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 25 Feb 2025 00:22:39 +0100 Subject: [PATCH 002/145] fix: allow double hyphen css selector names (#15384) --- .changeset/fast-pants-decide.md | 5 +++++ packages/svelte/src/compiler/phases/1-parse/read/style.js | 4 ++-- .../svelte/tests/css/samples/double-hyphen/expected.css | 4 ++++ .../svelte/tests/css/samples/double-hyphen/input.svelte | 7 +++++++ 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 .changeset/fast-pants-decide.md create mode 100644 packages/svelte/tests/css/samples/double-hyphen/expected.css create mode 100644 packages/svelte/tests/css/samples/double-hyphen/input.svelte diff --git a/.changeset/fast-pants-decide.md b/.changeset/fast-pants-decide.md new file mode 100644 index 0000000000..396b80c0e0 --- /dev/null +++ b/.changeset/fast-pants-decide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow double hyphen css selector names diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 29e8a0e541..f8c39f1b1d 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -566,7 +566,7 @@ function read_attribute_value(parser) { } /** - * https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram * @param {Parser} parser */ function read_identifier(parser) { @@ -574,7 +574,7 @@ function read_identifier(parser) { let identifier = ''; - if (parser.match('--') || parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) { + if (parser.match_regex(REGEX_LEADING_HYPHEN_OR_DIGIT)) { e.css_expected_identifier(start); } diff --git a/packages/svelte/tests/css/samples/double-hyphen/expected.css b/packages/svelte/tests/css/samples/double-hyphen/expected.css new file mode 100644 index 0000000000..ab4921b657 --- /dev/null +++ b/packages/svelte/tests/css/samples/double-hyphen/expected.css @@ -0,0 +1,4 @@ + + .--foo.svelte-xyz { + color: red; + } diff --git a/packages/svelte/tests/css/samples/double-hyphen/input.svelte b/packages/svelte/tests/css/samples/double-hyphen/input.svelte new file mode 100644 index 0000000000..e2907f9344 --- /dev/null +++ b/packages/svelte/tests/css/samples/double-hyphen/input.svelte @@ -0,0 +1,7 @@ +
+ + \ No newline at end of file From da98c894b82c9265c57f5a7a5d596369d7c1a05f Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:33:31 -0800 Subject: [PATCH 003/145] chore: switch to `tinyglobby` (#15388) * chore: switch to tinyglobby * update snapshot * update parameter name * changeset * delete changeset --------- Co-authored-by: Rich Harris --- packages/svelte/package.json | 4 +- packages/svelte/tests/helpers.js | 4 +- .../samples/script/expected_map.json | 2 +- .../svelte/tests/runtime-legacy/shared.ts | 4 +- packages/svelte/tests/snapshot/test.ts | 6 +- playgrounds/sandbox/package.json | 2 +- playgrounds/sandbox/run.js | 6 +- pnpm-lock.yaml | 247 +++++------------- 8 files changed, 77 insertions(+), 198 deletions(-) diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 399d908e7a..dd472e4413 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -137,11 +137,11 @@ "@rollup/plugin-virtual": "^3.0.2", "@types/aria-query": "^5.0.4", "@types/node": "^20.11.5", - "dts-buddy": "^0.5.3", + "dts-buddy": "^0.5.5", "esbuild": "^0.21.5", "rollup": "^4.22.4", "source-map": "^0.7.4", - "tiny-glob": "^0.2.9", + "tinyglobby": "^0.2.12", "typescript": "^5.5.4", "vitest": "^2.1.9" }, diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index 9d7f71c9bd..87bcb473e7 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -1,7 +1,7 @@ /** @import { CompileOptions } from '#compiler' */ import * as fs from 'node:fs'; import * as path from 'node:path'; -import glob from 'tiny-glob/sync.js'; +import { globSync } from 'tinyglobby'; import { VERSION, compile, compileModule, preprocess } from 'svelte/compiler'; import { vi } from 'vitest'; @@ -70,7 +70,7 @@ export async function compile_directory( fs.rmSync(output_dir, { recursive: true, force: true }); - for (let file of glob('**', { cwd, filesOnly: true })) { + for (let file of globSync('**', { cwd, onlyFiles: true })) { if (file.startsWith('_')) continue; let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r\n/g, '\n'); diff --git a/packages/svelte/tests/preprocess/samples/script/expected_map.json b/packages/svelte/tests/preprocess/samples/script/expected_map.json index d5bf98483f..22275b5338 100644 --- a/packages/svelte/tests/preprocess/samples/script/expected_map.json +++ b/packages/svelte/tests/preprocess/samples/script/expected_map.json @@ -1,6 +1,6 @@ { "version": 3, - "mappings": "AAAA,CAAC,MAAM,CAAC;AACR,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAc,CAAC,CAAC;AAC7B,CAAC,CAAC,MAAM", + "mappings": "AAAA,CAAC,MAAM;AACP,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAc,CAAC;AAC5B,CAAC,CAAC,MAAM", "names": [], "sources": [ "input.svelte" diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 76036852c2..3ffb3092a4 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { setImmediate } from 'node:timers/promises'; -import glob from 'tiny-glob/sync.js'; +import { globSync } from 'tinyglobby'; import { createClassComponent } from 'svelte/legacy'; import { proxy } from 'svelte/internal/client'; import { flushSync, hydrate, mount, unmount } from 'svelte'; @@ -257,7 +257,7 @@ async function run_test_variant( raf.reset(); // Put things we need on window for testing - const styles = glob('**/*.css', { cwd: `${cwd}/_output/client` }) + const styles = globSync('**/*.css', { cwd: `${cwd}/_output/client` }) .map((file) => fs.readFileSync(`${cwd}/_output/client/${file}`, 'utf-8')) .join('\n') .replace(/\/\*<\/?style>\*\//g, ''); diff --git a/packages/svelte/tests/snapshot/test.ts b/packages/svelte/tests/snapshot/test.ts index 88cf9193c3..0a591c6e2a 100644 --- a/packages/svelte/tests/snapshot/test.ts +++ b/packages/svelte/tests/snapshot/test.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import { assert, expect } from 'vitest'; -import glob from 'tiny-glob/sync.js'; +import { globSync } from 'tinyglobby'; import { compile_directory } from '../helpers.js'; import { suite, type BaseTest } from '../suite.js'; import { VERSION } from 'svelte/compiler'; @@ -18,8 +18,8 @@ const { test, run } = suite(async (config, cwd) => { fs.rmSync(`${cwd}/_expected`, { recursive: true, force: true }); fs.cpSync(`${cwd}/_output`, `${cwd}/_expected`, { recursive: true, force: true }); } else { - const actual = glob('**', { cwd: `${cwd}/_output`, filesOnly: true }); - const expected = glob('**', { cwd: `${cwd}/_expected`, filesOnly: true }); + const actual = globSync('**', { cwd: `${cwd}/_output`, onlyFiles: true }); + const expected = globSync('**', { cwd: `${cwd}/_expected`, onlyFiles: true }); assert.deepEqual(actual, expected); diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json index b5bd8ab2f9..9e2e5f6738 100644 --- a/playgrounds/sandbox/package.json +++ b/playgrounds/sandbox/package.json @@ -17,7 +17,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6", "polka": "^1.0.0-next.25", "svelte": "workspace:*", - "tiny-glob": "^0.2.9", + "tinyglobby": "^0.2.12", "vite": "^5.4.14", "vite-plugin-inspect": "^0.8.4" } diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 4cb8314532..8fa2c2a2da 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -2,7 +2,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; -import glob from 'tiny-glob/sync.js'; +import { globSync } from 'tinyglobby'; import { compile, compileModule, parse, migrate } from 'svelte/compiler'; const argv = parseArgs({ options: { runes: { type: 'boolean' } }, args: process.argv.slice(2) }); @@ -35,8 +35,8 @@ function write(file, contents) { fs.writeFileSync(file, contents); } -const svelte_modules = glob('**/*.svelte', { cwd: `${cwd}/src` }); -const js_modules = glob('**/*.js', { cwd: `${cwd}/src` }); +const svelte_modules = globSync('**/*.svelte', { cwd: `${cwd}/src` }); +const js_modules = globSync('**/*.js', { cwd: `${cwd}/src` }); for (const generate of /** @type {const} */ (['client', 'server'])) { console.error(`\n--- generating ${generate} ---\n`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7700cd65f..2457a94041 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,10 +70,10 @@ importers: version: 1.0.6 acorn: specifier: ^8.12.1 - version: 8.12.1 + version: 8.14.0 acorn-typescript: specifier: ^1.4.13 - version: 1.4.13(acorn@8.12.1) + version: 1.4.13(acorn@8.14.0) aria-query: specifier: ^5.3.1 version: 5.3.1 @@ -97,7 +97,7 @@ importers: version: 3.0.0 magic-string: specifier: ^0.30.11 - version: 0.30.11 + version: 0.30.17 zimmerframe: specifier: ^1.1.2 version: 1.1.2 @@ -127,8 +127,8 @@ importers: specifier: ^20.11.5 version: 20.12.7 dts-buddy: - specifier: ^0.5.3 - version: 0.5.3(typescript@5.5.4) + specifier: ^0.5.5 + version: 0.5.5(typescript@5.5.4) esbuild: specifier: ^0.21.5 version: 0.21.5 @@ -138,9 +138,9 @@ importers: source-map: specifier: ^0.7.4 version: 0.7.4 - tiny-glob: - specifier: ^0.2.9 - version: 0.2.9 + tinyglobby: + specifier: ^0.2.12 + version: 0.2.12 typescript: specifier: ^5.5.4 version: 5.5.4 @@ -159,9 +159,9 @@ importers: svelte: specifier: workspace:* version: link:../../packages/svelte - tiny-glob: - specifier: ^0.2.9 - version: 0.2.9 + tinyglobby: + specifier: ^0.2.12 + version: 0.2.12 vite: specifier: ^5.4.14 version: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) @@ -402,22 +402,12 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -794,11 +784,6 @@ packages: peerDependencies: acorn: '>=8.9.0' - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.14.0: resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} @@ -877,10 +862,6 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -945,7 +926,7 @@ packages: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -970,15 +951,6 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - debug@4.3.6: - resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -1035,11 +1007,11 @@ packages: resolution: {integrity: sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==} engines: {node: '>=12'} - dts-buddy@0.5.3: - resolution: {integrity: sha512-wS2DC5T+F6R+sG/YNlJ21yn8CKVhy1QQlpKA34G+uO4PUXkwz+JQWbGcIryUByxoJgbH98O0dTGzE2RqsRR3KA==} + dts-buddy@0.5.5: + resolution: {integrity: sha512-Mu5PJuP7C+EqZIwDtW/bG1tVli1UFhRIyW/dERBVBYk28OviTkribu9S2LpDQ0HF2MbkqnjQIkbbE6HnepdNTQ==} hasBin: true peerDependencies: - typescript: '>=5.0.4 <5.6' + typescript: '>=5.0.4 <5.8' eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1208,8 +1180,8 @@ packages: fastq@1.16.0: resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} - fdir@6.3.0: - resolution: {integrity: sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==} + fdir@6.4.3: + resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -1220,10 +1192,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1299,20 +1267,10 @@ packages: resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==} engines: {node: '>=18'} - globals@15.9.0: - resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==} - engines: {node: '>=18'} - - globalyzer@0.1.0: - resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1585,9 +1543,6 @@ packages: lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -1633,17 +1588,9 @@ packages: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1753,9 +1700,6 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1813,10 +1757,6 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.1: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} @@ -1922,11 +1862,6 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} @@ -1996,9 +1931,6 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} - std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} @@ -2066,15 +1998,16 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - tiny-glob@0.2.9: - resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2375,7 +2308,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.1 '@changesets/assemble-release-plan@6.0.4': dependencies: @@ -2384,7 +2317,7 @@ snapshots: '@changesets/should-skip-package': 0.1.1 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 - semver: 7.6.3 + semver: 7.7.1 '@changesets/changelog-git@0.2.0': dependencies: @@ -2417,9 +2350,9 @@ snapshots: outdent: 0.5.0 p-limit: 2.3.0 package-manager-detector: 0.2.0 - picocolors: 1.1.0 + picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.1 spawndamnit: 2.0.0 term-size: 2.2.1 @@ -2441,8 +2374,8 @@ snapshots: dependencies: '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 - picocolors: 1.1.0 - semver: 7.6.3 + picocolors: 1.1.1 + semver: 7.7.1 '@changesets/get-github-info@0.5.2': dependencies: @@ -2472,7 +2405,7 @@ snapshots: '@changesets/logger@0.1.1': dependencies: - picocolors: 1.1.0 + picocolors: 1.1.1 '@changesets/parse@0.4.0': dependencies: @@ -2494,7 +2427,7 @@ snapshots: '@changesets/types': 6.0.0 fs-extra: 7.0.1 p-filter: 2.1.0 - picocolors: 1.1.0 + picocolors: 1.1.1 '@changesets/should-skip-package@0.1.1': dependencies: @@ -2581,18 +2514,11 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@9.9.1)': - dependencies: - eslint: 9.9.1 - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.4.1(eslint@9.9.1)': dependencies: eslint: 9.9.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.0': {} - '@eslint-community/regexpp@4.12.1': {} '@eslint/config-array@0.18.0': @@ -2700,9 +2626,9 @@ snapshots: '@rollup/pluginutils': 5.1.0(rollup@4.22.4) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.3.0(picomatch@4.0.2) + fdir: 6.4.3(picomatch@4.0.2) is-reference: 1.2.1 - magic-string: 0.30.11 + magic-string: 0.30.17 picomatch: 4.0.2 optionalDependencies: rollup: 4.22.4 @@ -2801,14 +2727,14 @@ snapshots: eslint-config-prettier: 9.1.0(eslint@9.9.1) eslint-plugin-n: 17.9.0(eslint@9.9.1) eslint-plugin-svelte: 2.38.0(eslint@9.9.1)(svelte@packages+svelte) - globals: 15.9.0 + globals: 15.14.0 typescript: 5.5.4 typescript-eslint: 8.2.0(eslint@9.9.1)(typescript@5.5.4) '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) - debug: 4.3.6 + debug: 4.4.0 svelte: link:packages/svelte vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: @@ -2817,10 +2743,10 @@ snapshots: '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) - debug: 4.3.6 + debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.11 + magic-string: 0.30.17 svelte: link:packages/svelte vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vitefu: 0.2.5(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) @@ -2859,7 +2785,7 @@ snapshots: '@typescript-eslint/eslint-plugin@8.2.0(@typescript-eslint/parser@8.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@eslint-community/regexpp': 4.11.0 + '@eslint-community/regexpp': 4.12.1 '@typescript-eslint/parser': 8.2.0(eslint@9.9.1)(typescript@5.5.4) '@typescript-eslint/scope-manager': 8.2.0 '@typescript-eslint/type-utils': 8.2.0(eslint@9.9.1)(typescript@5.5.4) @@ -2915,7 +2841,7 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.1 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: typescript: 5.5.4 @@ -2924,7 +2850,7 @@ snapshots: '@typescript-eslint/utils@8.2.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1) '@typescript-eslint/scope-manager': 8.2.0 '@typescript-eslint/types': 8.2.0 '@typescript-eslint/typescript-estree': 8.2.0(typescript@5.5.4) @@ -2942,14 +2868,14 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.6 + debug: 4.4.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 - magic-string: 0.30.11 + magic-string: 0.30.17 magicast: 0.3.4 - std-env: 3.7.0 + std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 vitest: 2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) @@ -2996,19 +2922,13 @@ snapshots: loupe: 3.1.3 tinyrainbow: 1.2.0 - acorn-jsx@5.3.2(acorn@8.12.1): - dependencies: - acorn: 8.12.1 - acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 - acorn-typescript@1.4.13(acorn@8.12.1): + acorn-typescript@1.4.13(acorn@8.14.0): dependencies: - acorn: 8.12.1 - - acorn@8.12.1: {} + acorn: 8.14.0 acorn@8.14.0: {} @@ -3077,14 +2997,9 @@ snapshots: dependencies: balanced-match: 1.0.2 - braces@3.0.2: - dependencies: - fill-range: 7.0.1 - braces@3.0.3: dependencies: fill-range: 7.1.1 - optional: true buffer-from@1.1.2: {} @@ -3171,10 +3086,6 @@ snapshots: dataloader@1.4.0: {} - debug@4.3.6: - dependencies: - ms: 2.1.2 - debug@4.4.0: dependencies: ms: 2.1.3 @@ -3209,16 +3120,15 @@ snapshots: dotenv@16.3.2: {} - dts-buddy@0.5.3(typescript@5.5.4): + dts-buddy@0.5.5(typescript@5.5.4): dependencies: '@jridgewell/source-map': 0.3.6 '@jridgewell/sourcemap-codec': 1.5.0 - globrex: 0.1.2 kleur: 4.1.5 locate-character: 3.0.0 - magic-string: 0.30.11 + magic-string: 0.30.17 sade: 1.8.1 - tiny-glob: 0.2.9 + tinyglobby: 0.2.12 ts-api-utils: 1.3.0(typescript@5.5.4) typescript: 5.5.4 @@ -3339,8 +3249,8 @@ snapshots: eslint@9.9.1: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) - '@eslint-community/regexpp': 4.11.0 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1) + '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.18.0 '@eslint/eslintrc': 3.1.0 '@eslint/js': 9.9.1 @@ -3350,7 +3260,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.6 + debug: 4.4.0 escape-string-regexp: 4.0.0 eslint-scope: 8.0.2 eslint-visitor-keys: 4.0.0 @@ -3380,8 +3290,8 @@ snapshots: espree@10.1.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) eslint-visitor-keys: 4.0.0 espree@9.6.1: @@ -3442,7 +3352,7 @@ snapshots: dependencies: reusify: 1.0.4 - fdir@6.3.0(picomatch@4.0.2): + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -3450,14 +3360,9 @@ snapshots: dependencies: flat-cache: 4.0.1 - fill-range@7.0.1: - dependencies: - to-regex-range: 5.0.1 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - optional: true find-up@4.1.0: dependencies: @@ -3538,10 +3443,6 @@ snapshots: globals@15.14.0: {} - globals@15.9.0: {} - - globalyzer@0.1.0: {} - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -3551,8 +3452,6 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globrex@0.1.2: {} - graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -3816,10 +3715,6 @@ snapshots: pseudomap: 1.0.2 yallist: 2.1.2 - magic-string@0.30.11: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -3832,13 +3727,13 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 merge2@1.4.1: {} micromatch@4.0.5: dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 mime-db@1.52.0: {} @@ -3861,12 +3756,8 @@ snapshots: mrmime@2.0.0: {} - ms@2.1.2: {} - ms@2.1.3: {} - nanoid@3.3.7: {} - nanoid@3.3.8: {} natural-compare@1.4.0: {} @@ -3955,8 +3846,6 @@ snapshots: perfect-debounce@1.0.0: {} - picocolors@1.1.0: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3998,12 +3887,6 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.4.47: - dependencies: - nanoid: 3.3.7 - picocolors: 1.1.0 - source-map-js: 1.2.1 - postcss@8.5.1: dependencies: nanoid: 3.3.8 @@ -4110,8 +3993,6 @@ snapshots: dependencies: xmlchars: 2.2.0 - semver@7.6.3: {} - semver@7.7.1: {} serialize-javascript@6.0.2: @@ -4166,8 +4047,6 @@ snapshots: stackback@0.0.2: {} - std-env@3.7.0: {} - std-env@3.8.0: {} string-width@4.2.3: @@ -4219,7 +4098,7 @@ snapshots: terser@5.27.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.12.1 + acorn: 8.14.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -4231,15 +4110,15 @@ snapshots: text-table@0.2.0: {} - tiny-glob@0.2.9: - dependencies: - globalyzer: 0.1.0 - globrex: 0.1.2 - tinybench@2.9.0: {} tinyexec@0.3.2: {} + tinyglobby@0.2.12: + dependencies: + fdir: 6.4.3(picomatch@4.0.2) + picomatch: 4.0.2 + tinypool@1.0.2: {} tinyrainbow@1.2.0: {} @@ -4335,12 +4214,12 @@ snapshots: dependencies: '@antfu/utils': 0.7.8 '@rollup/pluginutils': 5.1.0(rollup@4.22.4) - debug: 4.3.6 + debug: 4.4.0 error-stack-parser-es: 0.1.1 fs-extra: 11.2.0 open: 10.1.0 perfect-debounce: 1.0.0 - picocolors: 1.1.0 + picocolors: 1.1.1 sirv: 2.0.4 vite: 5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: @@ -4350,7 +4229,7 @@ snapshots: vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: esbuild: 0.21.5 - postcss: 8.4.47 + postcss: 8.5.1 rollup: 4.22.4 optionalDependencies: '@types/node': 20.12.7 From cf56973bf0f8b2c0e9c87a1ae5393edd42911b90 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Wed, 26 Feb 2025 11:33:44 +0100 Subject: [PATCH 004/145] fix: class:directive not working with $$restProps (#15389) * add spread to test * fix #15386 * do no set cssHash on non-scoped element * changeset --- .changeset/lemon-cougars-buy.md | 6 ++ .../client/visitors/shared/element.js | 4 +- .../server/visitors/shared/element.js | 7 +- .../client/dom/elements/attributes.js | 13 ++- .../src/internal/client/dom/elements/class.js | 8 +- .../class-directive-mutations/_config.js | 93 ++++++++++++++++++- .../class-directive-mutations/main.svelte | 7 +- 7 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 .changeset/lemon-cougars-buy.md diff --git a/.changeset/lemon-cougars-buy.md b/.changeset/lemon-cougars-buy.md new file mode 100644 index 0000000000..382b3cdeb2 --- /dev/null +++ b/.changeset/lemon-cougars-buy.md @@ -0,0 +1,6 @@ +--- +'svelte': patch +--- + +fix: class:directive not working with $$restProps #15386 +fix: spread add an useless cssHash on non-scoped element diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index fc5fd2cc4c..81a4b45288 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -88,7 +88,9 @@ export function build_set_attributes( element_id, is_dynamic ? attributes_id : b.literal(null), b.object(values), - context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), + element.metadata.scoped && + context.state.analysis.css.hash !== '' && + b.literal(context.state.analysis.css.hash), preserve_attribute_case, is_custom_element, is_ignored(element, 'hydration_attribute_changed') && b.true diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 57101af4b8..281d8f0617 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -368,9 +368,10 @@ function build_element_spread_attributes( }) ); - const css_hash = context.state.analysis.css.hash - ? b.literal(context.state.analysis.css.hash) - : b.null; + const css_hash = + element.metadata.scoped && context.state.analysis.css.hash + ? b.literal(context.state.analysis.css.hash) + : b.null; const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined]; context.state.template.push(b.call('$.spread_attributes', ...args)); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 151024e85c..dd408dcf87 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -326,8 +326,16 @@ export function set_attributes( continue; } + if (key === 'class') { + var is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml'; + set_class(element, is_html, value, css_hash, prev?.[CLASS], next[CLASS]); + current[key] = value; + current[CLASS] = next[CLASS]; + continue; + } + var prev_value = current[key]; - if (value === prev_value && key !== 'class') continue; + if (value === prev_value) continue; current[key] = value; @@ -377,9 +385,6 @@ export function set_attributes( // @ts-ignore element[`__${event_name}`] = undefined; } - } else if (key === 'class') { - var is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml'; - set_class(element, is_html, value, css_hash, prev?.[CLASS], next[CLASS]); } else if (key === 'style' && value != null) { element.style.cssText = value + ''; } else if (key === 'autofocus') { diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js index 3308709a24..7027c84f62 100644 --- a/packages/svelte/src/internal/client/dom/elements/class.js +++ b/packages/svelte/src/internal/client/dom/elements/class.js @@ -6,8 +6,8 @@ import { hydrating } from '../hydration.js'; * @param {boolean | number} is_html * @param {string | null} value * @param {string} [hash] - * @param {Record} [prev_classes] - * @param {Record} [next_classes] + * @param {Record} [prev_classes] + * @param {Record} [next_classes] * @returns {Record | undefined} */ export function set_class(dom, is_html, value, hash, prev_classes, next_classes) { @@ -34,12 +34,10 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes) // @ts-expect-error need to add __className to patched prototype dom.__className = value; } else if (next_classes) { - prev_classes ??= {}; - for (var key in next_classes) { var is_present = !!next_classes[key]; - if (is_present !== !!prev_classes[key]) { + if (prev_classes == null || is_present !== !!prev_classes[key]) { dom.classList.toggle(key, is_present); } } diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js index dd1bc6ac1a..076efee994 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js @@ -1,4 +1,4 @@ -import { flushSync } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { test } from '../../test'; // This test counts mutations on hydration @@ -16,7 +16,12 @@ export default test({ html: `
-
+
+ + + + +
@@ -25,19 +30,97 @@ export default test({ ssrHtml: `
-
+
+ + + + +
`, - async test({ assert, component, instance }) { + async test({ target, assert, component, instance }) { flushSync(); + tick(); assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']); component.foo = false; flushSync(); - assert.deepEqual(instance.get_and_clear_mutations(), ['DIV', 'SPAN', 'B', 'I']); + tick(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'SPAN', 'B', 'I', 'DIV', 'SPAN', 'B', 'I'], + 'first mutation' + ); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+ + + + +
+ + + +
+ ` + ); + + component.foo = true; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'SPAN', 'B', 'I', 'DIV', 'SPAN', 'B', 'I'], + 'second mutation' + ); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+ + + + +
+ + + +
+ ` + ); + + component.classname = 'another'; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'B', 'DIV', 'B'], + 'class mutation' + ); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+ + + + +
+ + + +
+ ` + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte index 825362dcaf..d748988e21 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte @@ -33,10 +33,15 @@
-
+
+ +
+ + +
From be82332ac851dc3491a72dfb7128588f98bea812 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 27 Feb 2025 04:42:51 -0500 Subject: [PATCH 006/145] chore: simplify `process_effects` (#15397) --- .../svelte/src/internal/client/runtime.js | 47 +++++++------------ 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fe4104c108..486c819f36 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -748,68 +748,57 @@ export function schedule_effect(signal) { * bitwise flag passed in only. The collected effects array will be populated with all the user * effects to be flushed. * - * @param {Effect} effect + * @param {Effect} root * @returns {Effect[]} */ -function process_effects(effect) { +function process_effects(root) { /** @type {Effect[]} */ var effects = []; - var current_effect = effect.first; + var effect = root.first; - main_loop: while (current_effect !== null) { - var flags = current_effect.f; + while (effect !== null) { + var flags = effect.f; var is_branch = (flags & BRANCH_EFFECT) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; - var sibling = current_effect.next; if (!is_skippable_branch && (flags & INERT) === 0) { if ((flags & EFFECT) !== 0) { - effects.push(current_effect); + effects.push(effect); } else if (is_branch) { - current_effect.f ^= CLEAN; + effect.f ^= CLEAN; } else { // Ensure we set the effect to be the active reaction // to ensure that unowned deriveds are correctly tracked // because we're flushing the current effect var previous_active_reaction = active_reaction; try { - active_reaction = current_effect; - if (check_dirtiness(current_effect)) { - update_effect(current_effect); + active_reaction = effect; + if (check_dirtiness(effect)) { + update_effect(effect); } } catch (error) { - handle_error(error, current_effect, null, current_effect.ctx); + handle_error(error, effect, null, effect.ctx); } finally { active_reaction = previous_active_reaction; } } - var child = current_effect.first; + var child = effect.first; if (child !== null) { - current_effect = child; + effect = child; continue; } } - if (sibling === null) { - let parent = current_effect.parent; + var parent = effect.parent; + effect = effect.next; - while (parent !== null) { - if (effect === parent) { - break main_loop; - } - var parent_sibling = parent.next; - if (parent_sibling !== null) { - current_effect = parent_sibling; - continue main_loop; - } - parent = parent.parent; - } + while (effect === null && parent !== null) { + effect = parent.next; + parent = parent.parent; } - - current_effect = sibling; } return effects; From fe343e9c50ee14fb189356fdef82bb234f97ffc5 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Thu, 27 Feb 2025 10:57:43 +0100 Subject: [PATCH 007/145] fix: catch error on @const tag in svelte:boundary in DEV mode (#15369) * fix: catch error on @const tag in svelte:boundary * changeset * edit changeset * test * fix * rollback * simpler way to do that * rollback * roolback again --------- Co-authored-by: paoloricciuti --- .changeset/sixty-cats-search.md | 5 ++ .../client/visitors/SvelteBoundary.js | 23 ++++++++- .../svelte-boundary-dev-const/_config.js | 33 +++++++++++++ .../svelte-boundary-dev-const/main.svelte | 49 +++++++++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 .changeset/sixty-cats-search.md create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/main.svelte diff --git a/.changeset/sixty-cats-search.md b/.changeset/sixty-cats-search.md new file mode 100644 index 0000000000..6ffd007494 --- /dev/null +++ b/.changeset/sixty-cats-search.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: catch error on @const tag in svelte:boundary in DEV mode diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 6da650a591..9228df9703 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -1,6 +1,7 @@ /** @import { BlockStatement, Statement, Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ +import { dev } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; /** @@ -35,6 +36,9 @@ export function SvelteBoundary(node, context) { /** @type {Statement[]} */ const external_statements = []; + /** @type {Statement[]} */ + const internal_statements = []; + const snippets_visits = []; // Capture the `failed` implicit snippet prop @@ -53,7 +57,20 @@ export function SvelteBoundary(node, context) { /** @type {Statement[]} */ const init = []; context.visit(child, { ...context.state, init }); - external_statements.push(...init); + + if (dev) { + // In dev we must separate the declarations from the code + // that eagerly evaluate the expression... + for (const statement of init) { + if (statement.type === 'VariableDeclaration') { + external_statements.push(statement); + } else { + internal_statements.push(statement); + } + } + } else { + external_statements.push(...init); + } } else { nodes.push(child); } @@ -63,6 +80,10 @@ export function SvelteBoundary(node, context) { const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); + if (dev && internal_statements.length) { + block.body.unshift(...internal_statements); + } + const boundary = b.stmt( b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block)) ); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/_config.js new file mode 100644 index 0000000000..3c0195ce34 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/_config.js @@ -0,0 +1,33 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// https://github.com/sveltejs/svelte/issues/15368 +export default test({ + compileOptions: { + dev: true + }, + + mode: ['client'], + + html: ` +

BOOM

+

BOOM

+
OK
+
OK
+ `, + + async test({ target, assert, component }) { + component.ok = false; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +

BOOM

+

BOOM

+

BOOM

+

BOOM

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/main.svelte new file mode 100644 index 0000000000..30e074c762 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-boundary-dev-const/main.svelte @@ -0,0 +1,49 @@ + + + +
{throwError()}
+ + {#snippet failed()} +

BOOM

+ {/snippet} +
+ + + {@const result = throwError()} +
{result}
+ + {#snippet failed()} +

BOOM

+ {/snippet} +
+ + +
{throwErrorOnUpdate()}
+ + {#snippet failed()} +

BOOM

+ {/snippet} +
+ + + {@const result = throwErrorOnUpdate()} +
{result}
+ + {#snippet failed()} +

BOOM

+ {/snippet} +
From e4987d2a9f36b33f3934725723f9c4cc998ef278 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:58:56 +0000 Subject: [PATCH 008/145] Version Packages (#15383) Co-authored-by: github-actions[bot] --- .changeset/fast-pants-decide.md | 5 ----- .changeset/lemon-cougars-buy.md | 6 ------ .changeset/sixty-cats-search.md | 5 ----- .changeset/spotty-drinks-tan.md | 5 ----- .changeset/two-dragons-camp.md | 5 ----- packages/svelte/CHANGELOG.md | 15 +++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 17 insertions(+), 28 deletions(-) delete mode 100644 .changeset/fast-pants-decide.md delete mode 100644 .changeset/lemon-cougars-buy.md delete mode 100644 .changeset/sixty-cats-search.md delete mode 100644 .changeset/spotty-drinks-tan.md delete mode 100644 .changeset/two-dragons-camp.md diff --git a/.changeset/fast-pants-decide.md b/.changeset/fast-pants-decide.md deleted file mode 100644 index 396b80c0e0..0000000000 --- a/.changeset/fast-pants-decide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow double hyphen css selector names diff --git a/.changeset/lemon-cougars-buy.md b/.changeset/lemon-cougars-buy.md deleted file mode 100644 index 382b3cdeb2..0000000000 --- a/.changeset/lemon-cougars-buy.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'svelte': patch ---- - -fix: class:directive not working with $$restProps #15386 -fix: spread add an useless cssHash on non-scoped element diff --git a/.changeset/sixty-cats-search.md b/.changeset/sixty-cats-search.md deleted file mode 100644 index 6ffd007494..0000000000 --- a/.changeset/sixty-cats-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: catch error on @const tag in svelte:boundary in DEV mode diff --git a/.changeset/spotty-drinks-tan.md b/.changeset/spotty-drinks-tan.md deleted file mode 100644 index 2150c8cffd..0000000000 --- a/.changeset/spotty-drinks-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow for duplicate `var` declarations diff --git a/.changeset/two-dragons-camp.md b/.changeset/two-dragons-camp.md deleted file mode 100644 index cb0635186f..0000000000 --- a/.changeset/two-dragons-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix : bug "$0 is not defined" on svelte:element with a function call on class diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 907c5a3534..cb33bc0d39 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,20 @@ # svelte +## 5.20.5 + +### Patch Changes + +- fix: allow double hyphen css selector names ([#15384](https://github.com/sveltejs/svelte/pull/15384)) + +- fix: class:directive not working with $restProps #15386 ([#15389](https://github.com/sveltejs/svelte/pull/15389)) + fix: spread add an useless cssHash on non-scoped element + +- fix: catch error on @const tag in svelte:boundary in DEV mode ([#15369](https://github.com/sveltejs/svelte/pull/15369)) + +- fix: allow for duplicate `var` declarations ([#15382](https://github.com/sveltejs/svelte/pull/15382)) + +- fix : bug "$0 is not defined" on svelte:element with a function call on class ([#15396](https://github.com/sveltejs/svelte/pull/15396)) + ## 5.20.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index dd472e4413..bff6b5adc0 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.20.4", + "version": "5.20.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e893def326..b77e0ea49e 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.20.4'; +export const VERSION = '5.20.5'; export const PUBLIC_VERSION = '5'; From 474c588067fa75f6785d9cfe5ac15ff8bbc829fe Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 27 Feb 2025 17:42:36 +0100 Subject: [PATCH 009/145] fix: disallow `bind:group` to snippet parameters (#15401) --- .changeset/hip-oranges-hang.md | 5 +++++ .../docs/98-reference/.generated/compile-errors.md | 6 ++++++ .../svelte/messages/compile-errors/template.md | 4 ++++ packages/svelte/src/compiler/errors.js | 9 +++++++++ .../phases/2-analyze/visitors/BindDirective.js | 4 ++++ .../bind-group-snippet-parameter/errors.json | 14 ++++++++++++++ .../bind-group-snippet-parameter/input.svelte | 3 +++ 7 files changed, 45 insertions(+) create mode 100644 .changeset/hip-oranges-hang.md create mode 100644 packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json create mode 100644 packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte diff --git a/.changeset/hip-oranges-hang.md b/.changeset/hip-oranges-hang.md new file mode 100644 index 0000000000..addbeafa9c --- /dev/null +++ b/.changeset/hip-oranges-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: disallow `bind:group` to snippet parameters diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index a4ecbb31d5..ea116014e7 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -84,6 +84,12 @@ Attribute values containing `{...}` must be enclosed in quote marks, unless the `bind:group` can only bind to an Identifier or MemberExpression ``` +### bind_group_invalid_snippet_parameter + +``` +Cannot `bind:group` to a snippet parameter +``` + ### bind_invalid_expression ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index d061416dbd..0569f63ad3 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -54,6 +54,10 @@ > `bind:group` can only bind to an Identifier or MemberExpression +## bind_group_invalid_snippet_parameter + +> Cannot `bind:group` to a snippet parameter + ## bind_invalid_expression > Can only bind to an Identifier or MemberExpression or a `{get, set}` pair diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 93eeee539c..677b99fcff 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -752,6 +752,15 @@ export function bind_group_invalid_expression(node) { e(node, 'bind_group_invalid_expression', `\`bind:group\` can only bind to an Identifier or MemberExpression\nhttps://svelte.dev/e/bind_group_invalid_expression`); } +/** + * Cannot `bind:group` to a snippet parameter + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function bind_group_invalid_snippet_parameter(node) { + e(node, 'bind_group_invalid_snippet_parameter', `Cannot \`bind:group\` to a snippet parameter\nhttps://svelte.dev/e/bind_group_invalid_snippet_parameter`); +} + /** * Can only bind to an Identifier or MemberExpression or a `{get, set}` pair * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 509fecf301..18ea79262b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -191,6 +191,10 @@ export function BindDirective(node, context) { throw new Error('Cannot find declaration for bind:group'); } + if (binding.kind === 'snippet') { + e.bind_group_invalid_snippet_parameter(node); + } + // Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group, // i.e. one of their declarations is referenced in the binding. This allows group bindings to work // correctly when referencing a variable declared in an EachBlock by using the index of the each block diff --git a/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json new file mode 100644 index 0000000000..15e762419f --- /dev/null +++ b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "bind_group_invalid_snippet_parameter", + "end": { + "column": 44, + "line": 2 + }, + "message": "Cannot `bind:group` to a snippet parameter", + "start": { + "column": 21, + "line": 2 + } + } +] diff --git a/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte new file mode 100644 index 0000000000..368484788a --- /dev/null +++ b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte @@ -0,0 +1,3 @@ +{#snippet test(group)} + +{/snippet} \ No newline at end of file From 2032049e47b97098c874195d837a16acea5bb42e Mon Sep 17 00:00:00 2001 From: adiGuba Date: Mon, 3 Mar 2025 17:38:41 +0100 Subject: [PATCH 010/145] chore: Reduce hydration comment for {:else if} (#15250) * rewrite else/if hydration * fix: bad index * reduce args on if_block() * restore the block * don't use isNan() * changeset --- .changeset/tough-steaks-travel.md | 5 +++ .../3-transform/client/visitors/Fragment.js | 4 +- .../3-transform/client/visitors/IfBlock.js | 25 +++++------ .../3-transform/server/visitors/IfBlock.js | 30 +++++++++----- .../src/internal/client/dom/blocks/if.js | 41 ++++++++++++++----- 5 files changed, 71 insertions(+), 34 deletions(-) create mode 100644 .changeset/tough-steaks-travel.md diff --git a/.changeset/tough-steaks-travel.md b/.changeset/tough-steaks-travel.md new file mode 100644 index 0000000000..6a9468b609 --- /dev/null +++ b/.changeset/tough-steaks-travel.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +chore: Reduce hydration comment for {:else if} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index e9cfd9c506..389a694741 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -48,7 +48,9 @@ export function Fragment(node, context) { const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement'; const is_single_child_not_needing_template = trimmed.length === 1 && - (trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement'); + (trimmed[0].type === 'SvelteFragment' || + trimmed[0].type === 'TitleElement' || + (trimmed[0].type === 'IfBlock' && trimmed[0].elseif)); const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index d658f9eaf8..0876fa30b6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; @@ -19,14 +19,21 @@ export function IfBlock(node, context) { let alternate_id; if (node.alternate) { - const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); alternate_id = context.state.scope.generate('alternate'); - statements.push(b.var(b.id(alternate_id), b.arrow([b.id('$$anchor')], alternate))); + const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); + const nodes = node.alternate.nodes; + + let alternate_args = [b.id('$$anchor')]; + if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) { + alternate_args.push(b.id('$$elseif')); + } + + statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate))); } /** @type {Expression[]} */ const args = [ - context.state.node, + node.elseif ? b.id('$$anchor') : context.state.node, b.arrow( [b.id('$$render')], b.block([ @@ -34,13 +41,7 @@ export function IfBlock(node, context) { /** @type {Expression} */ (context.visit(node.test)), b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), alternate_id - ? b.stmt( - b.call( - b.id('$$render'), - b.id(alternate_id), - node.alternate ? b.literal(false) : undefined - ) - ) + ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.literal(false))) : undefined ) ]) @@ -69,7 +70,7 @@ export function IfBlock(node, context) { // ...even though they're logically equivalent. In the first case, the // transition will only play when `y` changes, but in the second it // should play when `x` or `y` change — both are considered 'local' - args.push(b.literal(true)); + args.push(b.id('$$elseif')); } statements.push(b.stmt(b.call('$.if', ...args))); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index 4df09aa8b9..cbdd2cd8cc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement, Expression, IfStatement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js'; @@ -10,19 +10,29 @@ import { block_close, block_open } from './shared/utils.js'; * @param {ComponentContext} context */ export function IfBlock(node, context) { - const test = /** @type {Expression} */ (context.visit(node.test)); - const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); + consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open))); + let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent); - const alternate = node.alternate - ? /** @type {BlockStatement} */ (context.visit(node.alternate)) - : b.block([]); + context.state.template.push(if_statement, block_close); - consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open))); + let index = 1; + let alt = node.alternate; + while (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) { + const elseif = alt.nodes[0]; + const alternate = /** @type {BlockStatement} */ (context.visit(elseif.consequent)); + alternate.body.unshift( + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(``))) + ); + if_statement = if_statement.alternate = b.if( + /** @type {Expression} */ (context.visit(elseif.test)), + alternate + ); + alt = elseif.alternate; + } - alternate.body.unshift( + if_statement.alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]); + if_statement.alternate.body.unshift( b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE))) ); - - context.state.template.push(b.if(test, consequent, alternate), block_close); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 36790c05c1..423c436fe4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -9,16 +9,16 @@ import { set_hydrating } from '../hydration.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; -import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; +import { HYDRATION_START, HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; /** * @param {TemplateNode} node - * @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn - * @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local' + * @param {(branch: (fn: (anchor: Node, elseif?: [number,number]) => void, flag?: boolean) => void) => void} fn + * @param {[number,number]} [elseif] * @returns {void} */ -export function if_block(node, fn, elseif = false) { - if (hydrating) { +export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) { + if (hydrating && root_index === 0) { hydrate_next(); } @@ -33,26 +33,44 @@ export function if_block(node, fn, elseif = false) { /** @type {UNINITIALIZED | boolean | null} */ var condition = UNINITIALIZED; - var flags = elseif ? EFFECT_TRANSPARENT : 0; + var flags = root_index > 0 ? EFFECT_TRANSPARENT : 0; var has_branch = false; - const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => { + const set_branch = ( + /** @type {(anchor: Node, elseif?: [number,number]) => void} */ fn, + flag = true + ) => { has_branch = true; update_branch(flag, fn); }; const update_branch = ( /** @type {boolean | null} */ new_condition, - /** @type {null | ((anchor: Node) => void)} */ fn + /** @type {null | ((anchor: Node, elseif?: [number,number]) => void)} */ fn ) => { if (condition === (condition = new_condition)) return; /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ let mismatch = false; - if (hydrating) { - const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE; + if (hydrating && hydrate_index !== -1) { + if (root_index === 0) { + const data = /** @type {Comment} */ (anchor).data; + if (data === HYDRATION_START) { + hydrate_index = 0; + } else if (data === HYDRATION_START_ELSE) { + hydrate_index = Infinity; + } else { + hydrate_index = parseInt(data.substring(1)); + if (hydrate_index !== hydrate_index) { + // if hydrate_index is NaN + // we set an invalid index to force mismatch + hydrate_index = condition ? Infinity : -1; + } + } + } + const is_else = hydrate_index > root_index; if (!!condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. @@ -62,6 +80,7 @@ export function if_block(node, fn, elseif = false) { set_hydrate_node(anchor); set_hydrating(false); mismatch = true; + hydrate_index = -1; // ignore hydration in next else if } } @@ -81,7 +100,7 @@ export function if_block(node, fn, elseif = false) { if (alternate_effect) { resume_effect(alternate_effect); } else if (fn) { - alternate_effect = branch(() => fn(anchor)); + alternate_effect = branch(() => fn(anchor, [root_index + 1, hydrate_index])); } if (consequent_effect) { From 7ce2dfc62297a41b8e403803cd54a86caf177418 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:40:08 +0000 Subject: [PATCH 011/145] Version Packages (#15402) Co-authored-by: github-actions[bot] --- .changeset/hip-oranges-hang.md | 5 ----- .changeset/tough-steaks-travel.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/hip-oranges-hang.md delete mode 100644 .changeset/tough-steaks-travel.md diff --git a/.changeset/hip-oranges-hang.md b/.changeset/hip-oranges-hang.md deleted file mode 100644 index addbeafa9c..0000000000 --- a/.changeset/hip-oranges-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: disallow `bind:group` to snippet parameters diff --git a/.changeset/tough-steaks-travel.md b/.changeset/tough-steaks-travel.md deleted file mode 100644 index 6a9468b609..0000000000 --- a/.changeset/tough-steaks-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -chore: Reduce hydration comment for {:else if} diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index cb33bc0d39..f32f872ee0 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.21.0 + +### Minor Changes + +- chore: Reduce hydration comment for {:else if} ([#15250](https://github.com/sveltejs/svelte/pull/15250)) + +### Patch Changes + +- fix: disallow `bind:group` to snippet parameters ([#15401](https://github.com/sveltejs/svelte/pull/15401)) + ## 5.20.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index bff6b5adc0..f32466ce85 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.20.5", + "version": "5.21.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b77e0ea49e..46fbec674c 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.20.5'; +export const VERSION = '5.21.0'; export const PUBLIC_VERSION = '5'; From b82692af2f032407e34137e4fe17aea76b8bf7f1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Mar 2025 05:49:31 -0500 Subject: [PATCH 012/145] feat: add `idPrefix` to `render` (#15428) This ensures someone like Astro can render multiple islands and the unique ids produced in them are able to be deduplicated --------- Co-authored-by: Hugos68 Co-authored-by: paoloricciuti Co-authored-by: Simon Holthausen --- .changeset/wise-grapes-enjoy.md | 5 ++ packages/svelte/src/index.d.ts | 1 + .../src/internal/client/dom/template.js | 14 ++--- .../svelte/src/internal/disclose-version.js | 7 ++- packages/svelte/src/internal/server/index.js | 13 ++-- packages/svelte/src/server/index.d.ts | 12 +++- packages/svelte/tests/hydration/test.ts | 11 ++-- .../svelte/tests/runtime-browser/assert.js | 1 + .../tests/runtime-browser/driver-ssr.js | 2 +- .../svelte/tests/runtime-browser/test-ssr.ts | 2 +- .../svelte/tests/runtime-legacy/shared.ts | 10 +++- .../samples/props-id-prefix/Child.svelte | 5 ++ .../samples/props-id-prefix/_config.js | 60 +++++++++++++++++++ .../samples/props-id-prefix/main.svelte | 19 ++++++ .../tests/server-side-rendering/test.ts | 3 +- packages/svelte/types/index.d.ts | 13 +++- 16 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 .changeset/wise-grapes-enjoy.md create mode 100644 packages/svelte/tests/runtime-runes/samples/props-id-prefix/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-id-prefix/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-id-prefix/main.svelte diff --git a/.changeset/wise-grapes-enjoy.md b/.changeset/wise-grapes-enjoy.md new file mode 100644 index 0000000000..05996232db --- /dev/null +++ b/.changeset/wise-grapes-enjoy.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: Add `idPrefix` option to `render` diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index 554510542e..38e6086689 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -19,6 +19,7 @@ export interface ComponentConstructorOptions< intro?: boolean; recover?: boolean; sync?: boolean; + idPrefix?: string; $$inline?: boolean; } diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 575bf55cf6..de2df62c92 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -250,12 +250,6 @@ export function append(anchor, dom) { anchor.before(/** @type {Node} */ (dom)); } -let uid = 1; - -export function reset_props_id() { - uid = 1; -} - /** * Create (or hydrate) an unique UID for the component instance. */ @@ -264,12 +258,16 @@ export function props_id() { hydrating && hydrate_node && hydrate_node.nodeType === 8 && - hydrate_node.textContent?.startsWith('#s') + hydrate_node.textContent?.startsWith(`#`) ) { const id = hydrate_node.textContent.substring(1); hydrate_next(); return id; } - return 'c' + uid++; + // @ts-expect-error This way we ensure the id is unique even across Svelte runtimes + (window.__svelte ??= {}).uid ??= 1; + + // @ts-expect-error + return `c${window.__svelte.uid++}`; } diff --git a/packages/svelte/src/internal/disclose-version.js b/packages/svelte/src/internal/disclose-version.js index 5e72f1e216..86c3482b5d 100644 --- a/packages/svelte/src/internal/disclose-version.js +++ b/packages/svelte/src/internal/disclose-version.js @@ -1,5 +1,6 @@ import { PUBLIC_VERSION } from '../version.js'; -if (typeof window !== 'undefined') - // @ts-ignore - (window.__svelte ||= { v: new Set() }).v.add(PUBLIC_VERSION); +if (typeof window !== 'undefined') { + // @ts-expect-error + ((window.__svelte ??= {}).v ??= new Set()).add(PUBLIC_VERSION); +} diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 160a1faa65..2591dbe4ea 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -86,9 +86,14 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) */ export let on_destroy = []; -function props_id_generator() { +/** + * Creates an ID generator + * @param {string} prefix + * @returns {() => string} + */ +function props_id_generator(prefix) { let uid = 1; - return () => 's' + uid++; + return () => `${prefix}s${uid++}`; } /** @@ -96,11 +101,11 @@ function props_id_generator() { * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props * @param {import('svelte').Component | ComponentType>} component - * @param {{ props?: Omit; context?: Map }} [options] + * @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options] * @returns {RenderOutput} */ export function render(component, options = {}) { - const uid = props_id_generator(); + const uid = props_id_generator(options.idPrefix ? options.idPrefix + '-' : ''); /** @type {Payload} */ const payload = { out: '', diff --git a/packages/svelte/src/server/index.d.ts b/packages/svelte/src/server/index.d.ts index b65ce5bdaa..d5a3b813e6 100644 --- a/packages/svelte/src/server/index.d.ts +++ b/packages/svelte/src/server/index.d.ts @@ -12,10 +12,18 @@ export function render< ...args: {} extends Props ? [ component: Comp extends SvelteComponent ? ComponentType : Comp, - options?: { props?: Omit; context?: Map } + options?: { + props?: Omit; + context?: Map; + idPrefix?: string; + } ] : [ component: Comp extends SvelteComponent ? ComponentType : Comp, - options: { props: Omit; context?: Map } + options: { + props: Omit; + context?: Map; + idPrefix?: string; + } ] ): RenderOutput; diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index 3bf2dd286c..266ac07bff 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -2,9 +2,9 @@ import * as fs from 'node:fs'; import { assert } from 'vitest'; -import { compile_directory, should_update_expected } from '../helpers.js'; +import { compile_directory } from '../helpers.js'; import { assert_html_equal } from '../html_equal.js'; -import { suite, assert_ok, type BaseTest } from '../suite.js'; +import { assert_ok, suite, type BaseTest } from '../suite.js'; import { createClassComponent } from 'svelte/legacy'; import { render } from 'svelte/server'; import type { CompileOptions } from '#compiler'; @@ -13,6 +13,7 @@ import { flushSync } from 'svelte'; interface HydrationTest extends BaseTest { load_compiled?: boolean; server_props?: Record; + id_prefix?: string; props?: Record; compileOptions?: Partial; /** @@ -50,7 +51,8 @@ const { test, run } = suite(async (config, cwd) => { const head = window.document.head; const rendered = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, { - props: config.server_props ?? config.props ?? {} + props: config.server_props ?? config.props ?? {}, + idPrefix: config?.id_prefix }); const override = read(`${cwd}/_override.html`); @@ -103,7 +105,8 @@ const { test, run } = suite(async (config, cwd) => { component: (await import(`${cwd}/_output/client/main.svelte.js`)).default, target, hydrate: true, - props: config.props + props: config.props, + idPrefix: config?.id_prefix }); console.warn = warn; diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js index 249d19f809..fb460c722a 100644 --- a/packages/svelte/tests/runtime-browser/assert.js +++ b/packages/svelte/tests/runtime-browser/assert.js @@ -119,6 +119,7 @@ function normalize_children(node) { * skip_mode?: Array<'server' | 'client' | 'hydrate'>; * html?: string; * ssrHtml?: string; + * id_prefix?: string; * props?: Props; * compileOptions?: Partial; * test?: (args: { diff --git a/packages/svelte/tests/runtime-browser/driver-ssr.js b/packages/svelte/tests/runtime-browser/driver-ssr.js index f5f15b6493..7067e48a1f 100644 --- a/packages/svelte/tests/runtime-browser/driver-ssr.js +++ b/packages/svelte/tests/runtime-browser/driver-ssr.js @@ -6,5 +6,5 @@ import config from '__CONFIG__'; import { render } from 'svelte/server'; export default function () { - return render(SvelteComponent, { props: config.props || {} }); + return render(SvelteComponent, { props: config.props || {}, idPrefix: config?.id_prefix }); } diff --git a/packages/svelte/tests/runtime-browser/test-ssr.ts b/packages/svelte/tests/runtime-browser/test-ssr.ts index 2ff1659f80..6987fac915 100644 --- a/packages/svelte/tests/runtime-browser/test-ssr.ts +++ b/packages/svelte/tests/runtime-browser/test-ssr.ts @@ -20,7 +20,7 @@ export async function run_ssr_test( await compile_directory(test_dir, 'server', config.compileOptions); const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default; - const { body } = render(Component, { props: config.props || {} }); + const { body } = render(Component, { props: config.props || {}, idPrefix: config.id_prefix }); fs.writeFileSync(`${test_dir}/_output/rendered.html`, body); diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 3ffb3092a4..fc748ce6b2 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -11,7 +11,6 @@ import { setup_html_equal } from '../html_equal.js'; import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; import { suite_with_variants, type BaseTest } from '../suite.js'; -import { reset_props_id } from '../../src/internal/client/dom/template.js'; type Assert = typeof import('vitest').assert & { htmlEqual(a: string, b: string, description?: string): void; @@ -37,6 +36,7 @@ export interface RuntimeTest = Record; props?: Props; server_props?: Props; + id_prefix?: string; before_test?: () => void; after_test?: () => void; test?: (args: { @@ -285,7 +285,8 @@ async function run_test_variant( // ssr into target const SsrSvelteComponent = (await import(`${cwd}/_output/server/main.svelte.js`)).default; const { html, head } = render(SsrSvelteComponent, { - props: config.server_props ?? config.props ?? {} + props: config.server_props ?? config.props ?? {}, + idPrefix: config.id_prefix }); fs.writeFileSync(`${cwd}/_output/rendered.html`, html); @@ -346,7 +347,10 @@ async function run_test_variant( if (runes) { props = proxy({ ...(config.props || {}) }); - reset_props_id(); + + // @ts-expect-error + globalThis.__svelte.uid = 1; + if (manual_hydrate) { hydrate_fn = () => { instance = hydrate(mod.default, { diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-prefix/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/Child.svelte new file mode 100644 index 0000000000..ad8bbd6f01 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/Child.svelte @@ -0,0 +1,5 @@ + + +

{id}

diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-prefix/_config.js b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/_config.js new file mode 100644 index 0000000000..6d4306c413 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/_config.js @@ -0,0 +1,60 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + id_prefix: 'myPrefix', + test({ assert, target, variant }) { + if (variant === 'dom') { + assert.htmlEqual( + target.innerHTML, + ` + +

c1

+

c2

+

c3

+

c4

+ ` + ); + } else { + assert.htmlEqual( + target.innerHTML, + ` + +

myPrefix-s1

+

myPrefix-s2

+

myPrefix-s3

+

myPrefix-s4

+ ` + ); + } + + let button = target.querySelector('button'); + flushSync(() => button?.click()); + + if (variant === 'dom') { + assert.htmlEqual( + target.innerHTML, + ` + +

c1

+

c2

+

c3

+

c4

+

c5

+ ` + ); + } else { + assert.htmlEqual( + target.innerHTML, + ` + +

myPrefix-s1

+

myPrefix-s2

+

myPrefix-s3

+

myPrefix-s4

+

c1

+ ` + ); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-prefix/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/main.svelte new file mode 100644 index 0000000000..646bb2ebde --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id-prefix/main.svelte @@ -0,0 +1,19 @@ + + + + +

{id}

+ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts index f76c5b539f..3e57539427 100644 --- a/packages/svelte/tests/server-side-rendering/test.ts +++ b/packages/svelte/tests/server-side-rendering/test.ts @@ -15,6 +15,7 @@ import type { CompileOptions } from '#compiler'; interface SSRTest extends BaseTest { compileOptions?: Partial; props?: Record; + id_prefix?: string; withoutNormalizeHtml?: boolean; errors?: string[]; } @@ -33,7 +34,7 @@ const { test, run } = suite(async (config, test_dir) => { const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default; const expected_html = try_read_file(`${test_dir}/_expected.html`); - const rendered = render(Component, { props: config.props || {} }); + const rendered = render(Component, { props: config.props || {}, idPrefix: config.id_prefix }); const { body, head } = rendered; fs.writeFileSync(`${test_dir}/_output/rendered.html`, body); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 4c47661af8..c3dbdcac79 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -16,6 +16,7 @@ declare module 'svelte' { intro?: boolean; recover?: boolean; sync?: boolean; + idPrefix?: string; $$inline?: boolean; } @@ -2080,11 +2081,19 @@ declare module 'svelte/server' { ...args: {} extends Props ? [ component: Comp extends SvelteComponent ? ComponentType : Comp, - options?: { props?: Omit; context?: Map } + options?: { + props?: Omit; + context?: Map; + idPrefix?: string; + } ] : [ component: Comp extends SvelteComponent ? ComponentType : Comp, - options: { props: Omit; context?: Map } + options: { + props: Omit; + context?: Map; + idPrefix?: string; + } ] ): RenderOutput; interface RenderOutput { From e3dc396e01469635e68ac25675739de7baaea2f7 Mon Sep 17 00:00:00 2001 From: Tom Adler Date: Tue, 4 Mar 2025 12:09:24 +0100 Subject: [PATCH 013/145] fix: make dialog element and role interactive (#15429) Fixes #8283 Also see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/932 --- .changeset/gentle-humans-call.md | 5 +++++ .../phases/2-analyze/visitors/shared/a11y.js | 2 +- .../input.svelte | 1 + .../warnings.json | 20 +++++++++---------- 4 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 .changeset/gentle-humans-call.md diff --git a/.changeset/gentle-humans-call.md b/.changeset/gentle-humans-call.md new file mode 100644 index 0000000000..d3c6590e8a --- /dev/null +++ b/.changeset/gentle-humans-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make dialog element and role interactive diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js index 24a8e5122d..1f58a28cad 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js @@ -30,7 +30,7 @@ const non_interactive_roles = non_abstract_roles // 'generic' is meant to have no semantic meaning. // 'cell' is treated as CellRole by the AXObject which is interactive, so we treat 'cell' it as interactive as well. !['toolbar', 'tabpanel', 'generic', 'cell'].includes(name) && - !role?.superClass.some((classes) => classes.includes('widget')) + !role?.superClass.some((classes) => classes.includes('widget') || classes.includes('window')) ); }) .concat( diff --git a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/input.svelte index 1adea90ba3..759f969d82 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/input.svelte @@ -3,6 +3,7 @@
{}} on:keypress={() => {}}>
+ {}}>alert

{}}>Heading

Heading

diff --git a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/warnings.json b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/warnings.json index 760866d136..e8bcd0cc1a 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/warnings.json +++ b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-interactions/warnings.json @@ -3,60 +3,60 @@ "code": "a11y_no_noninteractive_element_interactions", "end": { "column": 51, - "line": 10 + "line": 11 }, "message": "Non-interactive element `
` should not be assigned mouse or keyboard event listeners", "start": { "column": 0, - "line": 10 + "line": 11 } }, { "code": "a11y_no_noninteractive_element_interactions", "end": { "column": 58, - "line": 11 + "line": 12 }, "message": "Non-interactive element `

` should not be assigned mouse or keyboard event listeners", "start": { "column": 0, - "line": 11 + "line": 12 } }, { "code": "a11y_no_noninteractive_element_interactions", "end": { "column": 50, - "line": 12 + "line": 13 }, "message": "Non-interactive element `

` should not be assigned mouse or keyboard event listeners", "start": { "column": 0, - "line": 12 + "line": 13 } }, { "code": "a11y_no_noninteractive_element_interactions", "end": { "column": 30, - "line": 13 + "line": 14 }, "message": "Non-interactive element `

` should not be assigned mouse or keyboard event listeners", "start": { "column": 0, - "line": 13 + "line": 14 } }, { "code": "a11y_no_noninteractive_element_interactions", "end": { "column": 50, - "line": 14 + "line": 15 }, "message": "Non-interactive element `

` should not be assigned mouse or keyboard event listeners", "start": { "column": 0, - "line": 14 + "line": 15 } } ] From 0f2633736d4a6997456a368157115e1fa3781cca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:26:13 +0100 Subject: [PATCH 014/145] Version Packages (#15431) Co-authored-by: github-actions[bot] --- .changeset/gentle-humans-call.md | 5 ----- .changeset/wise-grapes-enjoy.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/gentle-humans-call.md delete mode 100644 .changeset/wise-grapes-enjoy.md diff --git a/.changeset/gentle-humans-call.md b/.changeset/gentle-humans-call.md deleted file mode 100644 index d3c6590e8a..0000000000 --- a/.changeset/gentle-humans-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: make dialog element and role interactive diff --git a/.changeset/wise-grapes-enjoy.md b/.changeset/wise-grapes-enjoy.md deleted file mode 100644 index 05996232db..0000000000 --- a/.changeset/wise-grapes-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: Add `idPrefix` option to `render` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f32f872ee0..6f6526c10b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.22.0 + +### Minor Changes + +- feat: Add `idPrefix` option to `render` ([#15428](https://github.com/sveltejs/svelte/pull/15428)) + +### Patch Changes + +- fix: make dialog element and role interactive ([#15429](https://github.com/sveltejs/svelte/pull/15429)) + ## 5.21.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f32466ce85..82d180d946 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.21.0", + "version": "5.22.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 46fbec674c..4b9540ad2a 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.21.0'; +export const VERSION = '5.22.0'; export const PUBLIC_VERSION = '5'; From ef7fd96ccf06aa6442db56a0871131163a7016ab Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 4 Mar 2025 14:36:44 +0100 Subject: [PATCH 015/145] fix: switch to our fork acorn typescript plugin (#15393) * fix: switch to our fork acorn typescript plugin * changeset * bump * bump * bump * adjust snapshots * this should be obsolete now * bump * bump --- .changeset/selfish-cougars-allow.md | 5 ++ packages/svelte/package.json | 2 +- .../src/compiler/phases/1-parse/acorn.js | 47 +------------------ .../src/compiler/phases/1-parse/ambient.d.ts | 3 -- .../samples/comment-before-script/output.json | 2 +- .../samples/snippets/output.json | 2 +- .../typescript-in-event-handler/output.json | 8 ++-- packages/svelte/tsconfig.json | 1 - pnpm-lock.yaml | 24 +++++----- 9 files changed, 26 insertions(+), 68 deletions(-) create mode 100644 .changeset/selfish-cougars-allow.md delete mode 100644 packages/svelte/src/compiler/phases/1-parse/ambient.d.ts diff --git a/.changeset/selfish-cougars-allow.md b/.changeset/selfish-cougars-allow.md new file mode 100644 index 0000000000..8cea0e52df --- /dev/null +++ b/.changeset/selfish-cougars-allow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: switch acorn-typescript plugin diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 82d180d946..b756719956 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -150,7 +150,7 @@ "@jridgewell/sourcemap-codec": "^1.5.0", "@types/estree": "^1.0.5", "acorn": "^8.12.1", - "acorn-typescript": "^1.4.13", + "@sveltejs/acorn-typescript": "^1.0.5", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 6c106e0a84..36f7688c49 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -1,11 +1,9 @@ /** @import { Comment, Program } from 'estree' */ -/** @import { Node } from 'acorn' */ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; -import { tsPlugin } from 'acorn-typescript'; -import { locator } from '../../state.js'; +import { tsPlugin } from '@sveltejs/acorn-typescript'; -const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true })); +const ParserWithTS = acorn.Parser.extend(tsPlugin()); /** * @param {string} source @@ -48,7 +46,6 @@ export function parse(source, typescript, is_script) { } } - if (typescript) amend(source, ast); add_comments(ast); return /** @type {Program} */ (ast); @@ -71,7 +68,6 @@ export function parse_expression_at(source, typescript, index) { locations: true }); - if (typescript) amend(source, ast); add_comments(ast); return ast; @@ -173,42 +169,3 @@ function get_comment_handlers(source) { } }; } - -/** - * Tidy up some stuff left behind by acorn-typescript - * @param {string} source - * @param {Node} node - */ -function amend(source, node) { - return walk(node, null, { - _(node, context) { - // @ts-expect-error - delete node.loc.start.index; - // @ts-expect-error - delete node.loc.end.index; - - if (typeof node.loc?.end === 'number') { - const loc = locator(node.loc.end); - if (loc) { - node.loc.end = { - line: loc.line, - column: loc.column - }; - } - } - - if ( - /** @type {any} */ (node).typeAnnotation && - (node.end === undefined || node.end < node.start) - ) { - // i think there might be a bug in acorn-typescript that prevents - // `end` from being assigned when there's a type annotation - let end = /** @type {any} */ (node).typeAnnotation.start; - while (/\s/.test(source[end - 1])) end -= 1; - node.end = end; - } - - context.next(); - } - }); -} diff --git a/packages/svelte/src/compiler/phases/1-parse/ambient.d.ts b/packages/svelte/src/compiler/phases/1-parse/ambient.d.ts deleted file mode 100644 index 8243dd0a42..0000000000 --- a/packages/svelte/src/compiler/phases/1-parse/ambient.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Silence the acorn typescript errors through this ambient type definition + tsconfig.json path alias -// That way we can omit `"skipLibCheck": true` and catch other errors in our d.ts files -declare module 'acorn-typescript'; 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 index 13c099a857..1aca0ce036 100644 --- a/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json +++ b/packages/svelte/tests/parser-modern/samples/comment-before-script/output.json @@ -75,7 +75,7 @@ "id": { "type": "Identifier", "start": 52, - "end": 57, + "end": 65, "loc": { "start": { "line": 3, diff --git a/packages/svelte/tests/parser-modern/samples/snippets/output.json b/packages/svelte/tests/parser-modern/samples/snippets/output.json index 2cf2596b18..acf484d8ae 100644 --- a/packages/svelte/tests/parser-modern/samples/snippets/output.json +++ b/packages/svelte/tests/parser-modern/samples/snippets/output.json @@ -28,7 +28,7 @@ { "type": "Identifier", "start": 43, - "end": 46, + "end": 54, "loc": { "start": { "line": 3, diff --git a/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json index 5d72e01b0a..9c515ad905 100644 --- a/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json +++ b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json @@ -25,7 +25,6 @@ "end": 147, "type": "OnDirective", "name": "click", - "modifiers": [], "expression": { "type": "ArrowFunctionExpression", "start": 73, @@ -48,7 +47,7 @@ { "type": "Identifier", "start": 74, - "end": 75, + "end": 87, "loc": { "start": { "line": 6, @@ -155,7 +154,7 @@ "id": { "type": "Identifier", "start": 102, - "end": 106, + "end": 114, "loc": { "start": { "line": 7, @@ -316,7 +315,8 @@ } ] } - } + }, + "modifiers": [] } ], "fragment": { diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index c9f0fb3b2b..76005add13 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -15,7 +15,6 @@ "allowJs": true, "checkJs": true, "paths": { - "acorn-typescript": ["./src/compiler/phases/1-parse/ambient.d.ts"], "svelte": ["./src/index.d.ts"], "svelte/action": ["./src/action/public.d.ts"], "svelte/compiler": ["./src/compiler/public.d.ts"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2457a94041..9ea9c9e929 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,15 +65,15 @@ importers: '@jridgewell/sourcemap-codec': specifier: ^1.5.0 version: 1.5.0 + '@sveltejs/acorn-typescript': + specifier: ^1.0.5 + version: 1.0.5(acorn@8.14.0) '@types/estree': specifier: ^1.0.5 version: 1.0.6 acorn: specifier: ^8.12.1 version: 8.14.0 - acorn-typescript: - specifier: ^1.4.13 - version: 1.4.13(acorn@8.14.0) aria-query: specifier: ^5.3.1 version: 5.3.1 @@ -626,6 +626,11 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@sveltejs/acorn-typescript@1.0.5': + resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} + peerDependencies: + acorn: ^8.9.0 + '@sveltejs/eslint-config@8.1.0': resolution: {integrity: sha512-cfgp4lPREYBjNd4ZzaP/jA85ufm7vfXiaV7h9vILXNogne80IbZRNhRCQ8XoOqTAOY/pChIzWTBuR8aDNMbAEA==} peerDependencies: @@ -779,11 +784,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-typescript@1.4.13: - resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==} - peerDependencies: - acorn: '>=8.9.0' - acorn@8.14.0: resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} @@ -2720,6 +2720,10 @@ snapshots: eslint-visitor-keys: 3.4.3 espree: 9.6.1 + '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.0)': + dependencies: + acorn: 8.14.0 + '@sveltejs/eslint-config@8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.9.0(eslint@9.9.1))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.2.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)': dependencies: '@stylistic/eslint-plugin-js': 1.8.0(eslint@9.9.1) @@ -2926,10 +2930,6 @@ snapshots: dependencies: acorn: 8.14.0 - acorn-typescript@1.4.13(acorn@8.14.0): - dependencies: - acorn: 8.14.0 - acorn@8.14.0: {} agent-base@7.1.1: From cd56c1d60eb5fb68af6618013be57f1a9fd9d55a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 09:30:57 -0500 Subject: [PATCH 016/145] Version Packages (#15433) Co-authored-by: github-actions[bot] --- .changeset/selfish-cougars-allow.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/selfish-cougars-allow.md diff --git a/.changeset/selfish-cougars-allow.md b/.changeset/selfish-cougars-allow.md deleted file mode 100644 index 8cea0e52df..0000000000 --- a/.changeset/selfish-cougars-allow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: switch acorn-typescript plugin diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6f6526c10b..5f031f8f99 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.22.1 + +### Patch Changes + +- chore: switch acorn-typescript plugin ([#15393](https://github.com/sveltejs/svelte/pull/15393)) + ## 5.22.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b756719956..3acd327682 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.22.0", + "version": "5.22.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 4b9540ad2a..4cea5c4b7f 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.22.0'; +export const VERSION = '5.22.1'; export const PUBLIC_VERSION = '5'; From b28b01350367110521d3a3eeae825ffe29ec0e2a Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:53:14 -0800 Subject: [PATCH 017/145] chore: speed up linting (#15424) --- package.json | 2 +- pnpm-lock.yaml | 208 ++++++++++++++++++++++++++----------------------- 2 files changed, 113 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index 2fe545b361..ad69bfc9ca 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "prettier-plugin-svelte": "^3.1.2", "svelte": "workspace:^", "typescript": "^5.5.4", - "typescript-eslint": "^8.2.0", + "typescript-eslint": "^8.24.0", "v8-natives": "^1.2.5", "vitest": "^2.1.9" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ea9c9e929..c687db12d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.27.8 '@sveltejs/eslint-config': specifier: ^8.1.0 - version: 8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.9.0(eslint@9.9.1))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.2.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4) + version: 8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4) '@svitejs/changesets-changelog-github-compact': specifier: ^1.1.0 version: 1.1.0 @@ -48,8 +48,8 @@ importers: specifier: ^5.5.4 version: 5.5.4 typescript-eslint: - specifier: ^8.2.0 - version: 8.2.0(eslint@9.9.1)(typescript@5.5.4) + specifier: ^8.24.0 + version: 8.26.0(eslint@9.9.1)(typescript@5.5.4) v8-natives: specifier: ^1.2.5 version: 1.2.5 @@ -688,61 +688,51 @@ packages: '@types/semver@7.5.6': resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} - '@typescript-eslint/eslint-plugin@8.2.0': - resolution: {integrity: sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==} + '@typescript-eslint/eslint-plugin@8.26.0': + resolution: {integrity: sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.2.0': - resolution: {integrity: sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==} + '@typescript-eslint/parser@8.26.0': + resolution: {integrity: sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.2.0': - resolution: {integrity: sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==} + '@typescript-eslint/scope-manager@8.26.0': + resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.2.0': - resolution: {integrity: sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==} + '@typescript-eslint/type-utils@8.26.0': + resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.2.0': - resolution: {integrity: sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==} + '@typescript-eslint/types@8.26.0': + resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.2.0': - resolution: {integrity: sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==} + '@typescript-eslint/typescript-estree@8.26.0': + resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.2.0': - resolution: {integrity: sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==} + '@typescript-eslint/utils@8.26.0': + resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.2.0': - resolution: {integrity: sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==} + '@typescript-eslint/visitor-keys@8.26.0': + resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/coverage-v8@2.0.5': @@ -1070,8 +1060,8 @@ packages: eslint-plugin-lube@0.4.3: resolution: {integrity: sha512-BVO83tRo090d6a04cl45Gb761SD79cOT6wKxxWrpsH7Rv8I0SJvc79ijE11vvyxxCMiGUVq/w4NqqPJAHyYfSQ==} - eslint-plugin-n@17.9.0: - resolution: {integrity: sha512-CPSaXDXdrT4nsrOrO4mT4VB6FMUkoySRkHWuuJJHVqsIEjIeZgMY1H7AzSwPbDScikBmLN82KeM1u7ixV7PzGg==} + eslint-plugin-n@17.16.1: + resolution: {integrity: sha512-/7FVAwjUrix9P5lycnsYRIQRwFo/DZROD+ZXWLpE+/EZWLyuLvyFaRdAPYJSz+nlAdZIZp+LAzlBerQSVYUNFg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: '>=8.23.0' @@ -1098,8 +1088,8 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint@9.9.1: @@ -1569,6 +1559,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2064,18 +2058,27 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-api-utils@2.0.1: + resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.5: + resolution: {integrity: sha512-WqmlO9IoeYwCqJ2E9kHMcY9GZhhfLYItC3VnHDlPOrg6nNdUWS4wn4hhDZUPt60m1EvtjPIZyprTjpI992Bgzw==} + peerDependencies: + typescript: '>=4.0.0' + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.2.0: - resolution: {integrity: sha512-DmnqaPcML0xYwUzgNbM1XaKXpEb7BShYf2P1tkUmmcl8hyeG7Pj08Er7R9bNy6AufabywzJcOybQAtnD/c9DGw==} + typescript-eslint@8.26.0: + resolution: {integrity: sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} @@ -2724,16 +2727,16 @@ snapshots: dependencies: acorn: 8.14.0 - '@sveltejs/eslint-config@8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.9.0(eslint@9.9.1))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.2.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)': + '@sveltejs/eslint-config@8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)': dependencies: '@stylistic/eslint-plugin-js': 1.8.0(eslint@9.9.1) eslint: 9.9.1 eslint-config-prettier: 9.1.0(eslint@9.9.1) - eslint-plugin-n: 17.9.0(eslint@9.9.1) + eslint-plugin-n: 17.16.1(eslint@9.9.1)(typescript@5.5.4) eslint-plugin-svelte: 2.38.0(eslint@9.9.1)(svelte@packages+svelte) globals: 15.14.0 typescript: 5.5.4 - typescript-eslint: 8.2.0(eslint@9.9.1)(typescript@5.5.4) + typescript-eslint: 8.26.0(eslint@9.9.1)(typescript@5.5.4) '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: @@ -2787,86 +2790,82 @@ snapshots: '@types/semver@7.5.6': {} - '@typescript-eslint/eslint-plugin@8.2.0(@typescript-eslint/parser@8.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.2.0 - '@typescript-eslint/type-utils': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/utils': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.2.0 + '@typescript-eslint/parser': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/type-utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.26.0 eslint: 9.9.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: + ts-api-utils: 2.0.1(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.2.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/parser@8.26.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@typescript-eslint/scope-manager': 8.2.0 - '@typescript-eslint/types': 8.2.0 - '@typescript-eslint/typescript-estree': 8.2.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.2.0 + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 eslint: 9.9.1 - optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.2.0': + '@typescript-eslint/scope-manager@8.26.0': dependencies: - '@typescript-eslint/types': 8.2.0 - '@typescript-eslint/visitor-keys': 8.2.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/type-utils@8.2.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@typescript-eslint/typescript-estree': 8.2.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.2.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) debug: 4.4.0 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: + eslint: 9.9.1 + ts-api-utils: 2.0.1(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - - eslint - supports-color - '@typescript-eslint/types@8.2.0': {} + '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/typescript-estree@8.2.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.26.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.2.0 - '@typescript-eslint/visitor-keys': 8.2.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 - globby: 11.1.0 + fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.1 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: + ts-api-utils: 2.0.1(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.2.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1) - '@typescript-eslint/scope-manager': 8.2.0 - '@typescript-eslint/types': 8.2.0 - '@typescript-eslint/typescript-estree': 8.2.0(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.5.4) eslint: 9.9.1 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@8.2.0': + '@typescript-eslint/visitor-keys@8.26.0': dependencies: - '@typescript-eslint/types': 8.2.0 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.26.0 + eslint-visitor-keys: 4.2.0 '@vitest/coverage-v8@2.0.5(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: @@ -3200,9 +3199,10 @@ snapshots: eslint-plugin-lube@0.4.3: {} - eslint-plugin-n@17.9.0(eslint@9.9.1): + eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4): dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1) + '@typescript-eslint/utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) enhanced-resolve: 5.18.1 eslint: 9.9.1 eslint-plugin-es-x: 7.8.0(eslint@9.9.1) @@ -3211,6 +3211,10 @@ snapshots: ignore: 5.3.2 minimatch: 9.0.5 semver: 7.7.1 + ts-declaration-location: 1.0.5(typescript@5.5.4) + transitivePeerDependencies: + - supports-color + - typescript eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte): dependencies: @@ -3245,7 +3249,7 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.0.0: {} + eslint-visitor-keys@4.2.0: {} eslint@9.9.1: dependencies: @@ -3263,7 +3267,7 @@ snapshots: debug: 4.4.0 escape-string-regexp: 4.0.0 eslint-scope: 8.0.2 - eslint-visitor-keys: 4.0.0 + eslint-visitor-keys: 4.2.0 espree: 10.1.0 esquery: 1.5.0 esutils: 2.0.3 @@ -3292,7 +3296,7 @@ snapshots: dependencies: acorn: 8.14.0 acorn-jsx: 5.3.2(acorn@8.14.0) - eslint-visitor-keys: 4.0.0 + eslint-visitor-keys: 4.2.0 espree@9.6.1: dependencies: @@ -3742,6 +3746,10 @@ snapshots: dependencies: mime-db: 1.52.0 + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -4161,19 +4169,27 @@ snapshots: dependencies: typescript: 5.5.4 + ts-api-utils@2.0.1(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + + ts-declaration-location@1.0.5(typescript@5.5.4): + dependencies: + minimatch: 10.0.1 + typescript: 5.5.4 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.2.0(eslint@9.9.1)(typescript@5.5.4): + typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 8.2.0(@typescript-eslint/parser@8.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/parser': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - '@typescript-eslint/utils': 8.2.0(eslint@9.9.1)(typescript@5.5.4) - optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.9.1)(typescript@5.5.4))(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/parser': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.26.0(eslint@9.9.1)(typescript@5.5.4) + eslint: 9.9.1 typescript: 5.5.4 transitivePeerDependencies: - - eslint - supports-color typescript@5.5.4: {} From 181fb2ad496dcd7e1915032b553115c805034bbf Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 5 Mar 2025 00:36:26 +0100 Subject: [PATCH 018/145] fix: correctly set `is_updating` before flushing root effects (#15442) * fix: correctly set `is_updating` before flushing root effects * rename for consistency with update_effect * use var --------- Co-authored-by: Rich Harris --- .changeset/proud-poems-brake.md | 5 +++ .../svelte/src/internal/client/runtime.js | 4 ++ packages/svelte/tests/signals/test.ts | 41 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 .changeset/proud-poems-brake.md diff --git a/.changeset/proud-poems-brake.md b/.changeset/proud-poems-brake.md new file mode 100644 index 0000000000..3f4e076e22 --- /dev/null +++ b/.changeset/proud-poems-brake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly set `is_updating` before flushing root effects diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 486c819f36..9f721f9ec4 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -644,8 +644,11 @@ function infinite_loop_guard() { } function flush_queued_root_effects() { + var was_updating_effect = is_updating_effect; + try { var flush_count = 0; + is_updating_effect = true; while (queued_root_effects.length > 0) { if (flush_count++ > 1000) { @@ -670,6 +673,7 @@ function flush_queued_root_effects() { } } finally { is_flushing = false; + is_updating_effect = was_updating_effect; last_scheduled_effect = null; if (DEV) { diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 046f833e0e..ef4cf16d3b 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1135,4 +1135,45 @@ describe('signals', () => { destroy(); }; }); + + test('unowned deriveds correctly update', () => { + const log: any[] = []; + + return () => { + const a = state(0); + const b = state(0); + const c = derived(() => { + return $.get(a); + }); + const d = derived(() => { + return $.get(b); + }); + + const destroy = effect_root(() => { + const e = derived(() => { + return $.get(c) === 1 && $.get(d) === 1; + }); + render_effect(() => { + log.push($.get(e)); + }); + }); + + assert.deepEqual(log, [false]); + + set(a, 1); + set(b, 1); + + flushSync(); + + assert.deepEqual(log, [false, true]); + + set(b, 9); + + flushSync(); + + assert.deepEqual(log, [false, true, false]); + + destroy(); + }; + }); }); From 016421533d2fca452597e9b955dccc53880599a5 Mon Sep 17 00:00:00 2001 From: Matthew Carroll <53098729+matthewCmatt@users.noreply.github.com> Date: Tue, 4 Mar 2025 21:53:46 -0500 Subject: [PATCH 019/145] Fix Rune phonetic spelling (#15445) --- documentation/docs/02-runes/01-what-are-runes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/01-what-are-runes.md b/documentation/docs/02-runes/01-what-are-runes.md index dc163ebdf1..59c371eb49 100644 --- a/documentation/docs/02-runes/01-what-are-runes.md +++ b/documentation/docs/02-runes/01-what-are-runes.md @@ -2,7 +2,7 @@ title: What are runes? --- -> [!NOTE] **rune** /ro͞on/ _noun_ +> [!NOTE] **rune** /ruːn/ _noun_ > > A letter or mark used as a mystical or magic symbol. From 3b465a717283a416a368071f07984285b8ea3f38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 21:55:34 -0500 Subject: [PATCH 020/145] Version Packages (#15444) Co-authored-by: github-actions[bot] --- .changeset/proud-poems-brake.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/proud-poems-brake.md diff --git a/.changeset/proud-poems-brake.md b/.changeset/proud-poems-brake.md deleted file mode 100644 index 3f4e076e22..0000000000 --- a/.changeset/proud-poems-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly set `is_updating` before flushing root effects diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 5f031f8f99..8327013969 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.22.2 + +### Patch Changes + +- fix: correctly set `is_updating` before flushing root effects ([#15442](https://github.com/sveltejs/svelte/pull/15442)) + ## 5.22.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 3acd327682..478668a602 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.22.1", + "version": "5.22.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 4cea5c4b7f..d00562ea4e 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.22.1'; +export const VERSION = '5.22.2'; export const PUBLIC_VERSION = '5'; From e591e872aa413282abb51995b97c7c5161693077 Mon Sep 17 00:00:00 2001 From: Pavel Nedrigailov <1223112+shadow-identity@users.noreply.github.com> Date: Wed, 5 Mar 2025 03:56:28 +0100 Subject: [PATCH 021/145] Update 01-basic-markup.md with new svelte-ignore syntax (#15394) --- documentation/docs/03-template-syntax/01-basic-markup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/03-template-syntax/01-basic-markup.md b/documentation/docs/03-template-syntax/01-basic-markup.md index b41dc187c3..5e8b4342d3 100644 --- a/documentation/docs/03-template-syntax/01-basic-markup.md +++ b/documentation/docs/03-template-syntax/01-basic-markup.md @@ -185,7 +185,7 @@ You can use HTML comments inside components. Comments beginning with `svelte-ignore` disable warnings for the next block of markup. Usually, these are accessibility warnings; make sure that you're disabling them for a good reason. ```svelte - + ``` From 3fc2007836d25b2e13d960ffa8ea26b5b197508e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Mar 2025 04:19:08 -0500 Subject: [PATCH 022/145] fix: run effect roots in tree order (#15446) * process effect roots in tree order * bring test over * add test * changeset * tidy --- .changeset/shy-falcons-occur.md | 5 ++++ .../src/internal/client/reactivity/effects.js | 24 ++++++++++++------- .../svelte/src/internal/client/runtime.js | 14 ++++------- .../samples/effect-root-5/_config.js | 17 +++++++++++++ .../samples/effect-root-5/main.svelte | 23 ++++++++++++++++++ .../samples/toStore-teardown/_config.js | 13 ++++++++++ .../samples/toStore-teardown/child.svelte | 11 +++++++++ .../samples/toStore-teardown/main.svelte | 15 ++++++++++++ 8 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 .changeset/shy-falcons-occur.md create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte diff --git a/.changeset/shy-falcons-occur.md b/.changeset/shy-falcons-occur.md new file mode 100644 index 0000000000..b16e28549e --- /dev/null +++ b/.changeset/shy-falcons-occur.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: run effect roots in tree order diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 28589ce94d..468bb94ab4 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -82,13 +82,12 @@ function push_effect(effect, parent_effect) { * @returns {Effect} */ function create_effect(type, fn, sync, push = true) { - var is_root = (type & ROOT_EFFECT) !== 0; - var parent_effect = active_effect; + var parent = active_effect; if (DEV) { // Ensure the parent is never an inspect effect - while (parent_effect !== null && (parent_effect.f & INSPECT_EFFECT) !== 0) { - parent_effect = parent_effect.parent; + while (parent !== null && (parent.f & INSPECT_EFFECT) !== 0) { + parent = parent.parent; } } @@ -103,7 +102,7 @@ function create_effect(type, fn, sync, push = true) { fn, last: null, next: null, - parent: is_root ? null : parent_effect, + parent, prev: null, teardown: null, transitions: null, @@ -136,9 +135,9 @@ function create_effect(type, fn, sync, push = true) { effect.teardown === null && (effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0; - if (!inert && !is_root && push) { - if (parent_effect !== null) { - push_effect(effect, parent_effect); + if (!inert && push) { + if (parent !== null) { + push_effect(effect, parent); } // if we're in a derived, add the effect there too @@ -391,7 +390,14 @@ export function destroy_effect_children(signal, remove_dom = false) { while (effect !== null) { var next = effect.next; - destroy_effect(effect, remove_dom); + + if ((effect.f & ROOT_EFFECT) !== 0) { + // this is now an independent root + effect.parent = null; + } else { + destroy_effect(effect, remove_dom); + } + effect = next; } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9f721f9ec4..bbe4dc3d9b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -661,13 +661,7 @@ function flush_queued_root_effects() { queued_root_effects = []; for (var i = 0; i < length; i++) { - var root = root_effects[i]; - - if ((root.f & CLEAN) === 0) { - root.f ^= CLEAN; - } - - var collected_effects = process_effects(root); + var collected_effects = process_effects(root_effects[i]); flush_queued_effects(collected_effects); } } @@ -759,11 +753,12 @@ function process_effects(root) { /** @type {Effect[]} */ var effects = []; - var effect = root.first; + /** @type {Effect | null} */ + var effect = root; while (effect !== null) { var flags = effect.f; - var is_branch = (flags & BRANCH_EFFECT) !== 0; + var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; if (!is_skippable_branch && (flags & INERT) === 0) { @@ -788,6 +783,7 @@ function process_effects(root) { } } + /** @type {Effect | null} */ var child = effect.first; if (child !== null) { diff --git a/packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js new file mode 100644 index 0000000000..260c757e3d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-root-5/_config.js @@ -0,0 +1,17 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [b1, b2] = target.querySelectorAll('button'); + + flushSync(() => b1.click()); + assert.deepEqual(logs, [0, 1]); + + flushSync(() => b1.click()); + assert.deepEqual(logs, [0, 1, 2]); + + flushSync(() => b2.click()); + assert.deepEqual(logs, [0, 1, 2]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte new file mode 100644 index 0000000000..06655a5362 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-root-5/main.svelte @@ -0,0 +1,23 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js new file mode 100644 index 0000000000..95904f011f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + let [, btn2] = target.querySelectorAll('button'); + + btn2.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte new file mode 100644 index 0000000000..f1b1b7b497 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/child.svelte @@ -0,0 +1,11 @@ + + +

+ Current value: + {$currentValue} +

diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte new file mode 100644 index 0000000000..7d36dd95cb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/toStore-teardown/main.svelte @@ -0,0 +1,15 @@ + + + + + +{#if data} + +{/if} From 0abd7f2a7f08d400c622d3e0a6da12cd041bd9f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:59:54 +0000 Subject: [PATCH 023/145] Version Packages (#15447) Co-authored-by: github-actions[bot] --- .changeset/shy-falcons-occur.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/shy-falcons-occur.md diff --git a/.changeset/shy-falcons-occur.md b/.changeset/shy-falcons-occur.md deleted file mode 100644 index b16e28549e..0000000000 --- a/.changeset/shy-falcons-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: run effect roots in tree order diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8327013969..e1bc27b51d 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.22.3 + +### Patch Changes + +- fix: run effect roots in tree order ([#15446](https://github.com/sveltejs/svelte/pull/15446)) + ## 5.22.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 478668a602..1657b59577 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.22.2", + "version": "5.22.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index d00562ea4e..29b56b5b2e 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.22.2'; +export const VERSION = '5.22.3'; export const PUBLIC_VERSION = '5'; From 76f5ecfdab2b65db9234d7fc2b59621a7b7c2dc5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Mar 2025 11:26:34 -0500 Subject: [PATCH 024/145] fix: never deduplicate expressions in templates (#15451) --- .changeset/five-wolves-swim.md | 5 ++ .../client/visitors/shared/utils.js | 46 ------------------- .../runtime-runes/samples/random/_config.js | 8 ++++ .../runtime-runes/samples/random/main.svelte | 6 +++ .../_expected/client/main.svelte.js | 6 +-- 5 files changed, 22 insertions(+), 49 deletions(-) create mode 100644 .changeset/five-wolves-swim.md create mode 100644 packages/svelte/tests/runtime-runes/samples/random/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/random/main.svelte diff --git a/.changeset/five-wolves-swim.md b/.changeset/five-wolves-swim.md new file mode 100644 index 0000000000..92178bed90 --- /dev/null +++ b/.changeset/five-wolves-swim.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: never deduplicate expressions in templates diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index c25ef3ab50..df6308d631 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -26,55 +26,9 @@ export function memoize_expression(state, value) { * @param {Expression} value */ export function get_expression_id(state, value) { - for (let i = 0; i < state.expressions.length; i += 1) { - if (compare_expressions(state.expressions[i], value)) { - return b.id(`$${i}`); - } - } - return b.id(`$${state.expressions.push(value) - 1}`); } -/** - * Returns true of two expressions have an identical AST shape - * @param {Expression} a - * @param {Expression} b - */ -function compare_expressions(a, b) { - if (a.type !== b.type) { - return false; - } - - for (const key in a) { - if (key === 'type' || key === 'metadata' || key === 'loc' || key === 'start' || key === 'end') { - continue; - } - - const va = /** @type {any} */ (a)[key]; - const vb = /** @type {any} */ (b)[key]; - - if ((typeof va === 'object') !== (typeof vb === 'object')) { - return false; - } - - if (typeof va !== 'object' || va === null || vb === null) { - if (va !== vb) return false; - } else if (Array.isArray(va)) { - if (va.length !== vb.length) { - return false; - } - - if (va.some((v, i) => !compare_expressions(v, vb[i]))) { - return false; - } - } else if (!compare_expressions(va, vb)) { - return false; - } - } - - return true; -} - /** * @param {Array} values * @param {(node: AST.SvelteNode, state: any) => any} visit diff --git a/packages/svelte/tests/runtime-runes/samples/random/_config.js b/packages/svelte/tests/runtime-runes/samples/random/_config.js new file mode 100644 index 0000000000..368dd20c6c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/random/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const [p1, p2] = target.querySelectorAll('p'); + assert.notEqual(p1.textContent, p2.textContent); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/random/main.svelte b/packages/svelte/tests/runtime-runes/samples/random/main.svelte new file mode 100644 index 0000000000..e1ec0b5649 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/random/main.svelte @@ -0,0 +1,6 @@ + + +

{(Math.random() * m).toFixed(10)}

+

{(Math.random() * m).toFixed(10)}

diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index d97a58bf40..219db6ffd5 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -21,13 +21,13 @@ export default function Main($$anchor) { $.template_effect(() => $.set_custom_element_data(custom_element_1, 'fooBar', y())); $.template_effect( - ($0) => { + ($0, $1) => { $.set_attribute(div, 'foobar', x); $.set_attribute(svg, 'viewBox', x); $.set_attribute(div_1, 'foobar', $0); - $.set_attribute(svg_1, 'viewBox', $0); + $.set_attribute(svg_1, 'viewBox', $1); }, - [y] + [y, y] ); $.append($$anchor, fragment); From 43ff3047ac525742c020565781b48dcd6bd06c39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:37:21 -0500 Subject: [PATCH 025/145] Version Packages (#15452) Co-authored-by: github-actions[bot] --- .changeset/five-wolves-swim.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/five-wolves-swim.md diff --git a/.changeset/five-wolves-swim.md b/.changeset/five-wolves-swim.md deleted file mode 100644 index 92178bed90..0000000000 --- a/.changeset/five-wolves-swim.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: never deduplicate expressions in templates diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index e1bc27b51d..0ff6b62fe0 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.22.4 + +### Patch Changes + +- fix: never deduplicate expressions in templates ([#15451](https://github.com/sveltejs/svelte/pull/15451)) + ## 5.22.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1657b59577..1f95811cd5 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.22.3", + "version": "5.22.4", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 29b56b5b2e..845375314f 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.22.3'; +export const VERSION = '5.22.4'; export const PUBLIC_VERSION = '5'; From 2d3818463a6ef4b888d6ca387725eb45e2143059 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Mar 2025 11:47:59 -0500 Subject: [PATCH 026/145] chore: check namespace inside set attributes (#15443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * set_attributes for * changeset * Update .changeset/wise-hats-wonder.md Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * add changeset * remove `chore` changeset — no need for non-user-facing changes to appear in changelog * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js * determine element traits inside set_attributes * unused * stash lookup --------- Co-authored-by: adiguba Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/quiet-baboons-listen.md | 5 ++ .../client/visitors/RegularElement.js | 11 +---- .../client/visitors/SvelteElement.js | 4 +- .../client/visitors/shared/element.js | 8 +-- packages/svelte/src/constants.js | 1 + .../client/dom/elements/attributes.js | 49 +++++++++++-------- 6 files changed, 37 insertions(+), 41 deletions(-) create mode 100644 .changeset/quiet-baboons-listen.md diff --git a/.changeset/quiet-baboons-listen.md b/.changeset/quiet-baboons-listen.md new file mode 100644 index 0000000000..eb5b4cc699 --- /dev/null +++ b/.changeset/quiet-baboons-listen.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: respect `svelte-ignore hydration_attribute_changed` on elements with spread attributes diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 434b49caa1..3dd3039213 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -221,16 +221,7 @@ export function RegularElement(node, context) { if (has_spread) { const attributes_id = b.id(context.state.scope.generate('attributes')); - build_set_attributes( - attributes, - class_directives, - context, - node, - node_id, - attributes_id, - (node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true, - is_custom_element_node(node) && b.true - ); + build_set_attributes(attributes, class_directives, context, node, node_id, attributes_id); // If value binding exists, that one takes care of calling $.init_select if (node.name === 'select' && !bindings.has('value')) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index ac284c818d..3250c24392 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -114,9 +114,7 @@ export function SvelteElement(node, context) { inner_context, node, element_id, - attributes_id, - b.binary('===', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')), - b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')) + attributes_id ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 81a4b45288..db8f2e4aa0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -17,8 +17,6 @@ import { build_template_chunk, get_expression_id } from './utils.js'; * @param {AST.RegularElement | AST.SvelteElement} element * @param {Identifier} element_id * @param {Identifier} attributes_id - * @param {false | Expression} preserve_attribute_case - * @param {false | Expression} is_custom_element */ export function build_set_attributes( attributes, @@ -26,9 +24,7 @@ export function build_set_attributes( context, element, element_id, - attributes_id, - preserve_attribute_case, - is_custom_element + attributes_id ) { let is_dynamic = false; @@ -91,8 +87,6 @@ export function build_set_attributes( element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), - preserve_attribute_case, - is_custom_element, is_ignored(element, 'hydration_attribute_changed') && b.true ); diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 03fddc5ebd..8861e440fc 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -33,6 +33,7 @@ export const UNINITIALIZED = Symbol(); export const FILENAME = Symbol('filename'); export const HMR = Symbol('hmr'); +export const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml'; export const NAMESPACE_SVG = 'http://www.w3.org/2000/svg'; export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML'; diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index dd408dcf87..44e67155fc 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -15,10 +15,14 @@ import { } from '../../runtime.js'; import { clsx } from '../../../shared/attributes.js'; import { set_class } from './class.js'; +import { NAMESPACE_HTML } from '../../../../constants.js'; export const CLASS = Symbol('class'); export const STYLE = Symbol('style'); +const IS_CUSTOM_ELEMENT = Symbol('is custom element'); +const IS_HTML = Symbol('is html'); + /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need * to remove it upon hydration to avoid a bug when someone resets the form value. @@ -63,8 +67,7 @@ export function remove_input_defaults(input) { * @param {any} value */ export function set_value(element, value) { - // @ts-expect-error - var attributes = (element.__attributes ??= {}); + var attributes = get_attributes(element); if ( attributes.value === @@ -87,8 +90,7 @@ export function set_value(element, value) { * @param {boolean} checked */ export function set_checked(element, checked) { - // @ts-expect-error - var attributes = (element.__attributes ??= {}); + var attributes = get_attributes(element); if ( attributes.checked === @@ -151,8 +153,7 @@ export function set_default_value(element, value) { * @param {boolean} [skip_warning] */ export function set_attribute(element, attribute, value, skip_warning) { - // @ts-expect-error - var attributes = (element.__attributes ??= {}); + var attributes = get_attributes(element); if (hydrating) { attributes[attribute] = element.getAttribute(attribute); @@ -261,20 +262,15 @@ export function set_custom_element_data(node, prop, value) { * @param {Record | undefined} prev * @param {Record} next New attributes - this function mutates this object * @param {string} [css_hash] - * @param {boolean} [preserve_attribute_case] - * @param {boolean} [is_custom_element] * @param {boolean} [skip_warning] * @returns {Record} */ -export function set_attributes( - element, - prev, - next, - css_hash, - preserve_attribute_case = false, - is_custom_element = false, - skip_warning = false -) { +export function set_attributes(element, prev, next, css_hash, skip_warning = false) { + var attributes = get_attributes(element); + + var is_custom_element = attributes[IS_CUSTOM_ELEMENT]; + var preserve_attribute_case = !attributes[IS_HTML]; + // If we're hydrating but the custom element is from Svelte, and it already scaffolded, // then it might run block logic in hydration mode, which we have to prevent. let is_hydrating_custom_element = hydrating && is_custom_element; @@ -299,9 +295,6 @@ export function set_attributes( var setters = get_setters(element); - // @ts-expect-error - var attributes = /** @type {Record} **/ (element.__attributes ??= {}); - // since key is captured we use const for (const key in next) { // let instead of var because referenced in a closure @@ -432,7 +425,7 @@ export function set_attributes( // @ts-ignore element[name] = value; } else if (typeof value !== 'function') { - set_attribute(element, name, value); + set_attribute(element, name, value, skip_warning); } } if (key === 'style' && '__styles' in element) { @@ -448,6 +441,20 @@ export function set_attributes( return current; } +/** + * + * @param {Element} element + */ +function get_attributes(element) { + return /** @type {Record} **/ ( + // @ts-expect-error + element.__attributes ??= { + [IS_CUSTOM_ELEMENT]: element.nodeName.includes('-'), + [IS_HTML]: element.namespaceURI === NAMESPACE_HTML + } + ); +} + /** @type {Map} */ var setters_cache = new Map(); From 1efad3f6e23442f296aa9910762512a42e450b87 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Mar 2025 12:26:18 -0500 Subject: [PATCH 027/145] chore: add monitoring to github actions (#15436) * chore: add monitoring to github actions * try this --- .github/workflows/ci.yml | 3 +++ .github/workflows/ecosystem-ci-trigger.yml | 1 + .github/workflows/pkg.pr.new-comment.yml | 1 + .github/workflows/pkg.pr.new.yml | 2 ++ .github/workflows/release.yml | 1 + 5 files changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2bcb08848..cf73a1f6cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ env: jobs: Tests: + permissions: {} runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: @@ -41,6 +42,7 @@ jobs: env: CI: true Lint: + permissions: {} runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -61,6 +63,7 @@ jobs: if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); } Benchmarks: + permissions: {} runs-on: ubuntu-latest timeout-minutes: 15 steps: diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index ce7bf04136..71df3242e8 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -9,6 +9,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - uses: actions/github-script@v6 with: script: | diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml index 1698a456d3..b1fba0b04b 100644 --- a/.github/workflows/pkg.pr.new-comment.yml +++ b/.github/workflows/pkg.pr.new-comment.yml @@ -11,6 +11,7 @@ jobs: name: 'Update comment' runs-on: ubuntu-latest steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: Download artifact uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 99f8153517..90d219faae 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -3,6 +3,8 @@ on: [push, pull_request] jobs: build: + permissions: {} + runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1daef0b89c..6debe5662a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ jobs: name: Release runs-on: ubuntu-latest steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: Checkout Repo uses: actions/checkout@v4 with: From 2f685c1dbadcf7e158282109f746400834f5eaf5 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Wed, 5 Mar 2025 20:20:56 +0100 Subject: [PATCH 028/145] fix: spreading style is not consistent with attribute (#15323) * style must be set via set_attribute * test * changeset * add empty string and null in test * explanatory comment * this is now redundant, set_attribute takes care of it * drive-by * tweak changeset --------- Co-authored-by: Rich Harris --- .changeset/real-cameras-attack.md | 5 ++ .../client/dom/elements/attributes.js | 17 ++--- .../samples/style-update/_config.js | 64 +++++++++++++++++++ .../samples/style-update/main.svelte | 9 +++ 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 .changeset/real-cameras-attack.md create mode 100644 packages/svelte/tests/runtime-runes/samples/style-update/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/style-update/main.svelte diff --git a/.changeset/real-cameras-attack.md b/.changeset/real-cameras-attack.md new file mode 100644 index 0000000000..35e2764785 --- /dev/null +++ b/.changeset/real-cameras-attack.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always use `setAttribute` when setting `style` diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 44e67155fc..cebc9173ba 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -218,6 +218,7 @@ export function set_custom_element_data(node, prop, value) { // or effect var previous_reaction = active_reaction; var previous_effect = active_effect; + // If we're hydrating but the custom element is from Svelte, and it already scaffolded, // then it might run block logic in hydration mode, which we have to prevent. let was_hydrating = hydrating; @@ -227,17 +228,20 @@ export function set_custom_element_data(node, prop, value) { set_active_reaction(null); set_active_effect(null); + try { if ( + // `style` should use `set_attribute` rather than the setter + prop !== 'style' && // Don't compute setters for custom elements while they aren't registered yet, // because during their upgrade/instantiation they might add more setters. // Instead, fall back to a simple "an object, then set as property" heuristic. - setters_cache.has(node.nodeName) || + (setters_cache.has(node.nodeName) || // customElements may not be available in browser extension contexts !customElements || customElements.get(node.tagName.toLowerCase()) ? get_setters(node).includes(prop) - : value && typeof value === 'object' + : value && typeof value === 'object') ) { // @ts-expect-error node[prop] = value; @@ -378,8 +382,9 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal // @ts-ignore element[`__${event_name}`] = undefined; } - } else if (key === 'style' && value != null) { - element.style.cssText = value + ''; + } else if (key === 'style') { + // avoid using the setter + set_attribute(element, key, value); } else if (key === 'autofocus') { autofocus(/** @type {HTMLElement} */ (element), Boolean(value)); } else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) { @@ -428,10 +433,6 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal set_attribute(element, name, value, skip_warning); } } - if (key === 'style' && '__styles' in element) { - // reset styles to force style: directive to update - element.__styles = {}; - } } if (is_hydrating_custom_element) { diff --git a/packages/svelte/tests/runtime-runes/samples/style-update/_config.js b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js new file mode 100644 index 0000000000..f0b7f2648e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js @@ -0,0 +1,64 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +const style_1 = 'invalid-key:0; margin:4px;;color: green ;color:blue '; +const style_2 = ' other-key : 0 ; padding:2px; COLOR:green; color: blue'; + +// https://github.com/sveltejs/svelte/issues/15309 +export default test({ + props: { + style: style_1 + }, + + html: ` +
+
+ + + + `, + + async test({ assert, target, component }) { + component.style = style_2; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+ + + + ` + ); + + component.style = ''; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+ + + + ` + ); + + component.style = null; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+ + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte new file mode 100644 index 0000000000..d29590d670 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-update/main.svelte @@ -0,0 +1,9 @@ + + +
+
+ + + From 30562b87802c7c814229c2774d2b264a89fda0d4 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Thu, 6 Mar 2025 01:34:10 +0100 Subject: [PATCH 029/145] chore: rewrite set_style() to handle directives (#15418) * add style attribute when needed * set_style() * to_style() * remove `style=""` * use cssTest for perfs * base test * test * changeset * revert dom.style.cssText * format name * use style.cssText + adapt test * Apply suggestions from code review suggestions from dummdidumm Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * fix priority * lint * yawn * update test * we can simplify some stuff now * simplify * more * simplify some more * more * more * more * more * more * remove continue * tweak * tweak * tweak * skip hash argument where possible * tweak * tweak * tweak * tweak --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/strange-planes-shout.md | 5 + .../src/compiler/phases/2-analyze/index.js | 26 +- .../client/visitors/RegularElement.js | 230 ++++++++---------- .../client/visitors/SvelteElement.js | 34 +-- .../client/visitors/shared/element.js | 136 +++++------ .../server/visitors/shared/element.js | 185 ++++++-------- .../client/dom/elements/attributes.js | 17 +- .../src/internal/client/dom/elements/style.js | 63 +++-- .../src/internal/client/dom/operations.js | 2 +- packages/svelte/src/internal/server/index.js | 46 ++-- .../svelte/src/internal/shared/attributes.js | 137 ++++++++++- .../_config.js | 2 +- .../_config.js | 2 +- .../main.svelte | 2 +- .../samples/dynamic-style-attr/_config.js | 4 +- .../samples/dynamic-style-attr/main.svelte | 2 +- .../style-directive-mutations/_config.js | 95 ++++++++ .../style-directive-mutations/main.svelte | 54 ++++ .../samples/style-update/_config.js | 11 +- 19 files changed, 654 insertions(+), 399 deletions(-) create mode 100644 .changeset/strange-planes-shout.md create mode 100644 packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte diff --git a/.changeset/strange-planes-shout.md b/.changeset/strange-planes-shout.md new file mode 100644 index 0000000000..58ef252740 --- /dev/null +++ b/.changeset/strange-planes-shout.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make `style:` directive and CSS handling more robust diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 322293bf6b..1f636c32df 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -769,17 +769,24 @@ export function analyze_component(root, source, options) { } let has_class = false; + let has_style = false; let has_spread = false; let has_class_directive = false; + let has_style_directive = false; for (const attribute of node.attributes) { // The spread method appends the hash to the end of the class attribute on its own if (attribute.type === 'SpreadAttribute') { has_spread = true; break; + } else if (attribute.type === 'Attribute') { + has_class ||= attribute.name.toLowerCase() === 'class'; + has_style ||= attribute.name.toLowerCase() === 'style'; + } else if (attribute.type === 'ClassDirective') { + has_class_directive = true; + } else if (attribute.type === 'StyleDirective') { + has_style_directive = true; } - has_class_directive ||= attribute.type === 'ClassDirective'; - has_class ||= attribute.type === 'Attribute' && attribute.name.toLowerCase() === 'class'; } // We need an empty class to generate the set_class() or class="" correctly @@ -796,6 +803,21 @@ export function analyze_component(root, source, options) { ]) ); } + + // We need an empty style to generate the set_style() correctly + if (!has_spread && !has_style && has_style_directive) { + node.attributes.push( + create_attribute('style', -1, -1, [ + { + type: 'Text', + data: '', + raw: '', + start: -1, + end: -1 + } + ]) + ); + } } // TODO diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 3dd3039213..6122dc4e0e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -1,4 +1,4 @@ -/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */ +/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { SourceLocation } from '#shared' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ @@ -20,9 +20,9 @@ import { build_getter } from '../utils.js'; import { get_attribute_name, build_attribute_value, - build_style_directives, build_set_attributes, - build_set_class + build_set_class, + build_set_style } from './shared/element.js'; import { process_children } from './shared/fragment.js'; import { @@ -215,13 +215,18 @@ export function RegularElement(node, context) { const node_id = context.state.node; - // Then do attributes - let is_attributes_reactive = has_spread; - if (has_spread) { const attributes_id = b.id(context.state.scope.generate('attributes')); - build_set_attributes(attributes, class_directives, context, node, node_id, attributes_id); + build_set_attributes( + attributes, + class_directives, + style_directives, + context, + node, + node_id, + attributes_id + ); // If value binding exists, that one takes care of calling $.init_select if (node.name === 'select' && !bindings.has('value')) { @@ -262,11 +267,13 @@ export function RegularElement(node, context) { } const name = get_attribute_name(node, attribute); + if ( !is_custom_element && !cannot_be_set_statically(attribute.name) && (attribute.value === true || is_text_attribute(attribute)) && - (name !== 'class' || class_directives.length === 0) + (name !== 'class' || class_directives.length === 0) && + (name !== 'style' || style_directives.length === 0) ) { let value = is_text_attribute(attribute) ? attribute.value[0].data : true; @@ -287,27 +294,30 @@ export function RegularElement(node, context) { }` ); } - continue; - } + } else if (name === 'autofocus') { + let { value } = build_attribute_value(attribute.value, context); + context.state.init.push(b.stmt(b.call('$.autofocus', node_id, value))); + } else if (name === 'class') { + const is_html = context.state.metadata.namespace === 'html' && node.name !== 'svg'; + build_set_class(node, node_id, attribute, class_directives, context, is_html); + } else if (name === 'style') { + build_set_style(node_id, attribute, style_directives, context); + } else if (is_custom_element) { + build_custom_element_attribute_update_assignment(node_id, attribute, context); + } else { + const { value, has_state } = build_attribute_value( + attribute.value, + context, + (value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value) + ); + + const update = build_element_attribute_update(node, node_id, name, value, attributes); - const is = - is_custom_element && name !== 'class' - ? build_custom_element_attribute_update_assignment(node_id, attribute, context) - : build_element_attribute_update_assignment( - node, - node_id, - attribute, - attributes, - class_directives, - context - ); - if (is) is_attributes_reactive = true; + (has_state ? context.state.update : context.state.init).push(b.stmt(update)); + } } } - // style directives must be applied last since they could override class/style attributes - build_style_directives(style_directives, node_id, context, is_attributes_reactive); - if ( is_load_error_element(node.name) && (has_spread || has_use || lookup.has('onload') || lookup.has('onerror')) @@ -519,6 +529,36 @@ export function build_class_directives_object(class_directives, context) { return b.object(properties); } +/** + * @param {AST.StyleDirective[]} style_directives + * @param {ComponentContext} context + * @return {ObjectExpression | ArrayExpression}} + */ +export function build_style_directives_object(style_directives, context) { + let normal_properties = []; + let important_properties = []; + + for (const directive of style_directives) { + const expression = + directive.value === true + ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) + : build_attribute_value(directive.value, context, (value, metadata) => + metadata.has_call ? get_expression_id(context.state, value) : value + ).value; + const property = b.init(directive.name, expression); + + if (directive.modifiers.includes('important')) { + important_properties.push(property); + } else { + normal_properties.push(property); + } + } + + return important_properties.length + ? b.array([b.object(normal_properties), b.object(important_properties)]) + : b.object(normal_properties); +} + /** * Serializes an assignment to an element property by adding relevant statements to either only * the init or the the init and update arrays, depending on whether or not the value is dynamic. @@ -543,73 +583,29 @@ export function build_class_directives_object(class_directives, context) { * Returns true if attribute is deemed reactive, false otherwise. * @param {AST.RegularElement} element * @param {Identifier} node_id - * @param {AST.Attribute} attribute + * @param {string} name + * @param {Expression} value * @param {Array} attributes - * @param {AST.ClassDirective[]} class_directives - * @param {ComponentContext} context - * @returns {boolean} */ -function build_element_attribute_update_assignment( - element, - node_id, - attribute, - attributes, - class_directives, - context -) { - const state = context.state; - const name = get_attribute_name(element, attribute); - const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; - const is_mathml = context.state.metadata.namespace === 'mathml'; - - const is_autofocus = name === 'autofocus'; - - let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call - ? // if it's autofocus we will not add this to a template effect so we don't want to get the expression id - // but separately memoize the expression - is_autofocus - ? memoize_expression(state, value) - : get_expression_id(state, value) - : value - ); +function build_element_attribute_update(element, node_id, name, value, attributes) { + if (name === 'muted') { + // Special case for Firefox who needs it set as a property in order to work + return b.assignment('=', b.member(node_id, b.id('muted')), value); + } - if (is_autofocus) { - state.init.push(b.stmt(b.call('$.autofocus', node_id, value))); - return false; + if (name === 'value') { + return b.call('$.set_value', node_id, value); } - // Special case for Firefox who needs it set as a property in order to work - if (name === 'muted') { - if (!has_state) { - state.init.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value))); - return false; - } - state.update.push(b.stmt(b.assignment('=', b.member(node_id, b.id('muted')), value))); - return false; + if (name === 'checked') { + return b.call('$.set_checked', node_id, value); } - /** @type {Statement} */ - let update; + if (name === 'selected') { + return b.call('$.set_selected', node_id, value); + } - if (name === 'class') { - return build_set_class( - element, - node_id, - attribute, - value, - has_state, - class_directives, - context, - !is_svg && !is_mathml - ); - } else if (name === 'value') { - update = b.stmt(b.call('$.set_value', node_id, value)); - } else if (name === 'checked') { - update = b.stmt(b.call('$.set_checked', node_id, value)); - } else if (name === 'selected') { - update = b.stmt(b.call('$.set_selected', node_id, value)); - } else if ( + if ( // If we would just set the defaultValue property, it would override the value property, // because it is set in the template which implicitly means it's also setting the default value, // and if one updates the default value while the input is pristine it will also update the @@ -620,62 +616,49 @@ function build_element_attribute_update_assignment( ) || (element.name === 'textarea' && element.fragment.nodes.length > 0)) ) { - update = b.stmt(b.call('$.set_default_value', node_id, value)); - } else if ( + return b.call('$.set_default_value', node_id, value); + } + + if ( // See defaultValue comment name === 'defaultChecked' && attributes.some( (attr) => attr.type === 'Attribute' && attr.name === 'checked' && attr.value === true ) ) { - update = b.stmt(b.call('$.set_default_checked', node_id, value)); - } else if (is_dom_property(name)) { - update = b.stmt(b.assignment('=', b.member(node_id, name), value)); - } else { - const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute'; - update = b.stmt( - b.call( - callee, - node_id, - b.literal(name), - value, - is_ignored(element, 'hydration_attribute_changed') && b.true - ) - ); + return b.call('$.set_default_checked', node_id, value); } - if (has_state) { - state.update.push(update); - return true; - } else { - state.init.push(update); - return false; + if (is_dom_property(name)) { + return b.assignment('=', b.member(node_id, name), value); } + + return b.call( + name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute', + node_id, + b.literal(name), + value, + is_ignored(element, 'hydration_attribute_changed') && b.true + ); } /** - * Like `build_element_attribute_update_assignment` but without any special attribute treatment. + * Like `build_element_attribute_update` but without any special attribute treatment. * @param {Identifier} node_id * @param {AST.Attribute} attribute * @param {ComponentContext} context - * @returns {boolean} */ function build_custom_element_attribute_update_assignment(node_id, attribute, context) { - const state = context.state; - const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive - let { value, has_state } = build_attribute_value(attribute.value, context); + const { value, has_state } = build_attribute_value(attribute.value, context); - const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value)); + // don't lowercase name, as we set the element's property, which might be case sensitive + const call = b.call('$.set_custom_element_data', node_id, b.literal(attribute.name), value); - if (has_state) { - // this is different from other updates — it doesn't get grouped, - // because set_custom_element_data may not be idempotent - state.init.push(b.stmt(b.call('$.template_effect', b.thunk(update.expression)))); - return true; - } else { - state.init.push(update); - return false; - } + // this is different from other updates — it doesn't get grouped, + // because set_custom_element_data may not be idempotent + const update = has_state ? b.call('$.template_effect', b.thunk(call)) : call; + + context.state.init.push(b.stmt(update)); } /** @@ -686,7 +669,6 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co * @param {Identifier} node_id * @param {AST.Attribute} attribute * @param {ComponentContext} context - * @returns {boolean} */ function build_element_special_value_attribute(element, node_id, attribute, context) { const state = context.state; @@ -699,7 +681,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont metadata.has_call ? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately is_select_with_value - ? memoize_expression(context.state, value) + ? memoize_expression(state, value) : get_expression_id(state, value) : value ); @@ -743,9 +725,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont value, update ); - return true; } else { state.init.push(update); - return false; } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index 3250c24392..115eb6ccc1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -5,12 +5,7 @@ import { dev, locator } from '../../../../state.js'; import { is_text_attribute } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; import { determine_namespace_for_children } from '../../utils.js'; -import { - build_attribute_value, - build_set_attributes, - build_set_class, - build_style_directives -} from './shared/element.js'; +import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js'; import { build_render_statement, get_expression_id } from './shared/utils.js'; /** @@ -77,40 +72,22 @@ export function SvelteElement(node, context) { // Let bindings first, they can be used on attributes context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot - // Then do attributes - let is_attributes_reactive = false; - if ( attributes.length === 1 && attributes[0].type === 'Attribute' && attributes[0].name.toLowerCase() === 'class' && is_text_attribute(attributes[0]) ) { - // special case when there only a class attribute, without call expression - let { value, has_state } = build_attribute_value( - attributes[0].value, - context, - (value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value) - ); - - is_attributes_reactive = build_set_class( - node, - element_id, - attributes[0], - value, - has_state, - class_directives, - inner_context, - false - ); + build_set_class(node, element_id, attributes[0], class_directives, inner_context, false); } else if (attributes.length) { const attributes_id = b.id(context.state.scope.generate('attributes')); // 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. - is_attributes_reactive = build_set_attributes( + build_set_attributes( attributes, class_directives, + style_directives, inner_context, node, element_id, @@ -118,9 +95,6 @@ export function SvelteElement(node, context) { ); } - // style directives must be applied last since they could override class/style attributes - build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive); - const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag))); if (dev) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index db8f2e4aa0..e0eb04d823 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -1,4 +1,4 @@ -/** @import { Expression, Identifier, ObjectExpression } from 'estree' */ +/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ import { escape_html } from '../../../../../../escaping.js'; @@ -6,13 +6,13 @@ import { normalize_attribute } from '../../../../../../utils.js'; import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { build_getter } from '../../utils.js'; -import { build_class_directives_object } from '../RegularElement.js'; +import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; import { build_template_chunk, get_expression_id } from './utils.js'; /** * @param {Array} attributes * @param {AST.ClassDirective[]} class_directives + * @param {AST.StyleDirective[]} style_directives * @param {ComponentContext} context * @param {AST.RegularElement | AST.SvelteElement} element * @param {Identifier} element_id @@ -21,6 +21,7 @@ import { build_template_chunk, get_expression_id } from './utils.js'; export function build_set_attributes( attributes, class_directives, + style_directives, context, element, element_id, @@ -79,6 +80,18 @@ export function build_set_attributes( class_directives.find((directive) => directive.metadata.expression.has_state) !== null; } + if (style_directives.length) { + values.push( + b.prop( + 'init', + b.array([b.id('$.STYLE')]), + build_style_directives_object(style_directives, context) + ) + ); + + is_dynamic ||= style_directives.some((directive) => directive.metadata.expression.has_state); + } + const call = b.call( '$.set_attributes', element_id, @@ -94,54 +107,8 @@ export function build_set_attributes( context.state.init.push(b.let(attributes_id)); const update = b.stmt(b.assignment('=', attributes_id, call)); context.state.update.push(update); - return true; - } - - context.state.init.push(b.stmt(call)); - return false; -} - -/** - * Serializes each style directive into something like `$.set_style(element, style_property, value)` - * and adds it either to init or update, depending on whether or not the value or the attributes are dynamic. - * @param {AST.StyleDirective[]} style_directives - * @param {Identifier} element_id - * @param {ComponentContext} context - * @param {boolean} is_attributes_reactive - */ -export function build_style_directives( - style_directives, - element_id, - context, - is_attributes_reactive -) { - const state = context.state; - - for (const directive of style_directives) { - const { has_state } = directive.metadata.expression; - - let value = - directive.value === true - ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) - : build_attribute_value(directive.value, context, (value, metadata) => - metadata.has_call ? get_expression_id(context.state, value) : value - ).value; - - const update = b.stmt( - b.call( - '$.set_style', - element_id, - b.literal(directive.name), - value, - /** @type {Expression} */ (directive.modifiers.includes('important') ? b.true : undefined) - ) - ); - - if (has_state || is_attributes_reactive) { - state.update.push(update); - } else { - state.init.push(update); - } + } else { + context.state.init.push(b.stmt(call)); } } @@ -189,24 +156,16 @@ export function get_attribute_name(element, attribute) { /** * @param {AST.RegularElement | AST.SvelteElement} element * @param {Identifier} node_id - * @param {AST.Attribute | null} attribute - * @param {Expression} value - * @param {boolean} has_state + * @param {AST.Attribute} attribute * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context * @param {boolean} is_html - * @returns {boolean} */ -export function build_set_class( - element, - node_id, - attribute, - value, - has_state, - class_directives, - context, - is_html -) { +export function build_set_class(element, node_id, attribute, class_directives, context, is_html) { + let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => + metadata.has_call ? get_expression_id(context.state, value) : value + ); + if (attribute && attribute.metadata.needs_clsx) { value = b.call('$.clsx', value); } @@ -265,13 +224,48 @@ export function build_set_class( set_class = b.assignment('=', previous_id, set_class); } - const update = b.stmt(set_class); + (has_state ? context.state.update : context.state.init).push(b.stmt(set_class)); +} - if (has_state) { - context.state.update.push(update); - return true; +/** + * @param {Identifier} node_id + * @param {AST.Attribute} attribute + * @param {AST.StyleDirective[]} style_directives + * @param {ComponentContext} context + */ +export function build_set_style(node_id, attribute, style_directives, context) { + let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => + metadata.has_call ? get_expression_id(context.state, value) : value + ); + + /** @type {Identifier | undefined} */ + let previous_id; + + /** @type {ObjectExpression | Identifier | undefined} */ + let prev; + + /** @type {ArrayExpression | ObjectExpression | undefined} */ + let next; + + if (style_directives.length) { + next = build_style_directives_object(style_directives, context); + has_state ||= style_directives.some((d) => d.metadata.expression.has_state); + + if (has_state) { + previous_id = b.id(context.state.scope.generate('styles')); + context.state.init.push(b.declaration('let', [b.declarator(previous_id)])); + prev = previous_id; + } else { + prev = b.object([]); + } + } + + /** @type {Expression} */ + let set_style = b.call('$.set_style', node_id, value, prev, next); + + if (previous_id) { + set_style = b.assignment('=', previous_id, set_style); } - context.state.init.push(update); - return false; + (has_state ? context.state.update : context.state.init).push(b.stmt(set_style)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 281d8f0617..4a5becfb2f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -1,4 +1,4 @@ -/** @import { Expression, Literal, ObjectExpression } from 'estree' */ +/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */ /** @import { AST, Namespace } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ import { @@ -48,9 +48,6 @@ export function build_element_attributes(node, context) { let content = null; let has_spread = false; - // Use the index to keep the attributes order which is important for spreading - let class_index = -1; - let style_index = -1; let events_to_capture = new Set(); for (const attribute of node.attributes) { @@ -86,7 +83,6 @@ export function build_element_attributes(node, context) { // the defaultValue/defaultChecked properties don't exist as attributes } else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') { if (attribute.name === 'class') { - class_index = attributes.length; if (attribute.metadata.needs_clsx) { attributes.push({ ...attribute, @@ -102,10 +98,6 @@ export function build_element_attributes(node, context) { attributes.push(attribute); } } else { - if (attribute.name === 'style') { - style_index = attributes.length; - } - attributes.push(attribute); } } @@ -212,41 +204,30 @@ export function build_element_attributes(node, context) { } } - if ((node.metadata.scoped || class_directives.length) && !has_spread) { - const class_attribute = build_to_class( - node.metadata.scoped ? context.state.analysis.css.hash : null, - class_directives, - /** @type {AST.Attribute | null} */ (attributes[class_index] ?? null) - ); - if (class_index === -1) { - attributes.push(class_attribute); - } - } - - if (style_directives.length > 0 && !has_spread) { - build_style_directives( - style_directives, - /** @type {AST.Attribute | null} */ (attributes[style_index] ?? null), - context - ); - if (style_index > -1) { - attributes.splice(style_index, 1); - } - } - if (has_spread) { build_element_spread_attributes(node, attributes, style_directives, class_directives, context); } else { + const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null; + for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { - if (attribute.value === true || is_text_attribute(attribute)) { - const name = get_attribute_name(node, attribute); - const literal_value = /** @type {Literal} */ ( + const name = get_attribute_name(node, attribute); + const can_use_literal = + (name !== 'class' || class_directives.length === 0) && + (name !== 'style' || style_directives.length === 0); + + if (can_use_literal && (attribute.value === true || is_text_attribute(attribute))) { + let literal_value = /** @type {Literal} */ ( build_attribute_value( attribute.value, context, WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) ) ).value; + + if (name === 'class' && css_hash) { + literal_value = (String(literal_value) + ' ' + css_hash).trim(); + } + if (name !== 'class' || literal_value) { context.state.template.push( b.literal( @@ -258,10 +239,10 @@ export function build_element_attributes(node, context) { ) ); } + continue; } - const name = get_attribute_name(node, attribute); const value = build_attribute_value( attribute.value, context, @@ -269,8 +250,15 @@ export function build_element_attributes(node, context) { ); // pre-escape and inline literal attributes : - if (value.type === 'Literal' && typeof value.value === 'string') { + if (can_use_literal && value.type === 'Literal' && typeof value.value === 'string') { + if (name === 'class' && css_hash) { + value.value = (value.value + ' ' + css_hash).trim(); + } context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`)); + } else if (name === 'class') { + context.state.template.push(build_attr_class(class_directives, value, context, css_hash)); + } else if (name === 'style') { + context.state.template.push(build_attr_style(style_directives, value, context)); } else { context.state.template.push( b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) @@ -379,100 +367,79 @@ function build_element_spread_attributes( /** * - * @param {string | null} hash * @param {AST.ClassDirective[]} class_directives - * @param {AST.Attribute | null} class_attribute - * @returns + * @param {Expression} expression + * @param {ComponentContext} context + * @param {string | null} hash */ -function build_to_class(hash, class_directives, class_attribute) { - if (class_attribute === null) { - class_attribute = create_attribute('class', -1, -1, []); - } - +function build_attr_class(class_directives, expression, context, hash) { /** @type {ObjectExpression | undefined} */ - let classes; + let directives; if (class_directives.length) { - classes = b.object( + directives = b.object( class_directives.map((directive) => - b.prop('init', b.literal(directive.name), directive.expression) + b.prop( + 'init', + b.literal(directive.name), + /** @type {Expression} */ (context.visit(directive.expression, context.state)) + ) ) ); } - /** @type {Expression} */ - let class_name; - - if (class_attribute.value === true) { - class_name = b.literal(''); - } else if (Array.isArray(class_attribute.value)) { - if (class_attribute.value.length === 0) { - class_name = b.null; - } else { - class_name = class_attribute.value - .map((val) => (val.type === 'Text' ? b.literal(val.data) : val.expression)) - .reduce((left, right) => b.binary('+', left, right)); - } - } else { - class_name = class_attribute.value.expression; - } + let css_hash; - /** @type {Expression} */ - let expression; - - if ( - hash && - !classes && - class_name.type === 'Literal' && - (class_name.value === null || class_name.value === '' || typeof class_name.value === 'string') - ) { - if (class_name.value === null || class_name.value === '') { - expression = b.literal(hash); + if (hash) { + if (expression.type === 'Literal' && typeof expression.value === 'string') { + expression.value = (expression.value + ' ' + hash).trim(); } else { - expression = b.literal(escape_html(class_name.value, true) + ' ' + hash); + css_hash = b.literal(hash); } - } else { - expression = b.call('$.to_class', class_name, b.literal(hash), classes); } - class_attribute.value = { - type: 'ExpressionTag', - start: -1, - end: -1, - expression: expression, - metadata: { - expression: create_expression_metadata() - } - }; - - return class_attribute; + return b.call('$.attr_class', expression, css_hash, directives); } /** + * * @param {AST.StyleDirective[]} style_directives - * @param {AST.Attribute | null} style_attribute + * @param {Expression} expression * @param {ComponentContext} context */ -function build_style_directives(style_directives, style_attribute, context) { - const styles = style_directives.map((directive) => { - let value = - directive.value === true - ? b.id(directive.name) - : build_attribute_value(directive.value, context, true); - if (directive.modifiers.includes('important')) { - value = b.binary('+', value, b.literal(' !important')); +function build_attr_style(style_directives, expression, context) { + /** @type {ArrayExpression | ObjectExpression | undefined} */ + let directives; + + if (style_directives.length) { + let normal_properties = []; + let important_properties = []; + + for (const directive of style_directives) { + const expression = + directive.value === true + ? b.id(directive.name) + : build_attribute_value(directive.value, context, true); + + let name = directive.name; + if (name[0] !== '-' || name[1] !== '-') { + name = name.toLowerCase(); + } + + const property = b.init(directive.name, expression); + if (directive.modifiers.includes('important')) { + important_properties.push(property); + } else { + normal_properties.push(property); + } } - return b.init(directive.name, value); - }); - - const arg = - style_attribute === null - ? b.object(styles) - : b.call( - '$.merge_styles', - build_attribute_value(style_attribute.value, context, true), - b.object(styles) - ); - context.state.template.push(b.call('$.add_styles', arg)); + if (important_properties.length) { + directives = b.array([b.object(normal_properties), b.object(important_properties)]); + } else { + directives = b.object(normal_properties); + } + } + + return b.call('$.attr_style', expression, directives); } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index cebc9173ba..5a5d5d7c9b 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -15,6 +15,7 @@ import { } from '../../runtime.js'; import { clsx } from '../../../shared/attributes.js'; import { set_class } from './class.js'; +import { set_style } from './style.js'; import { NAMESPACE_HTML } from '../../../../constants.js'; export const CLASS = Symbol('class'); @@ -177,11 +178,6 @@ export function set_attribute(element, attribute, value, skip_warning) { if (attributes[attribute] === (attributes[attribute] = value)) return; - if (attribute === 'style' && '__styles' in element) { - // reset styles to force style: directive to update - element.__styles = {}; - } - if (attribute === 'loading') { // @ts-expect-error element[LOADING_ATTR_SYMBOL] = value; @@ -297,6 +293,10 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal next.class = null; /* force call to set_class() */ } + if (next[STYLE]) { + next.style ??= null; /* force call to set_style() */ + } + var setters = get_setters(element); // since key is captured we use const @@ -331,6 +331,13 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal continue; } + if (key === 'style') { + set_style(element, value, prev?.[STYLE], next[STYLE]); + current[key] = value; + current[STYLE] = next[STYLE]; + continue; + } + var prev_value = current[key]; if (value === prev_value) continue; diff --git a/packages/svelte/src/internal/client/dom/elements/style.js b/packages/svelte/src/internal/client/dom/elements/style.js index 34531029c9..3e05eec30e 100644 --- a/packages/svelte/src/internal/client/dom/elements/style.js +++ b/packages/svelte/src/internal/client/dom/elements/style.js @@ -1,22 +1,57 @@ +import { to_style } from '../../../shared/attributes.js'; +import { hydrating } from '../hydration.js'; + /** - * @param {HTMLElement} dom - * @param {string} key - * @param {string} value - * @param {boolean} [important] + * @param {Element & ElementCSSInlineStyle} dom + * @param {Record} prev + * @param {Record} next + * @param {string} [priority] */ -export function set_style(dom, key, value, important) { - // @ts-expect-error - var styles = (dom.__styles ??= {}); +function update_styles(dom, prev = {}, next, priority) { + for (var key in next) { + var value = next[key]; - if (styles[key] === value) { - return; + if (prev[key] !== value) { + if (next[key] == null) { + dom.style.removeProperty(key); + } else { + dom.style.setProperty(key, value, priority); + } + } } +} - styles[key] = value; +/** + * @param {Element & ElementCSSInlineStyle} dom + * @param {string | null} value + * @param {Record | [Record, Record]} [prev_styles] + * @param {Record | [Record, Record]} [next_styles] + */ +export function set_style(dom, value, prev_styles, next_styles) { + // @ts-expect-error + var prev = dom.__style; + + if (hydrating || prev !== value) { + var next_style_attr = to_style(value, next_styles); - if (value == null) { - dom.style.removeProperty(key); - } else { - dom.style.setProperty(key, value, important ? 'important' : ''); + if (!hydrating || next_style_attr !== dom.getAttribute('style')) { + if (next_style_attr == null) { + dom.removeAttribute('style'); + } else { + dom.style.cssText = next_style_attr; + } + } + + // @ts-expect-error + dom.__style = value; + } else if (next_styles) { + if (Array.isArray(next_styles)) { + update_styles(dom, prev_styles?.[0], next_styles[0]); + update_styles(dom, prev_styles?.[1], next_styles[1], 'important'); + } else { + update_styles(dom, prev_styles, next_styles); + } } + + return next_styles; } diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index f6ac92456e..0ad9045b20 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -48,7 +48,7 @@ export function init_operations() { // @ts-expect-error element_prototype.__attributes = null; // @ts-expect-error - element_prototype.__styles = null; + element_prototype.__style = undefined; // @ts-expect-error element_prototype.__e = undefined; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 2591dbe4ea..6098b496c5 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -2,7 +2,7 @@ /** @import { Component, Payload, RenderOutput } from '#server' */ /** @import { Store } from '#shared' */ export { FILENAME, HMR } from '../../constants.js'; -import { attr, clsx, to_class } from '../shared/attributes.js'; +import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; import { is_promise, noop } from '../shared/utils.js'; import { subscribe_to_store } from '../../store/utils.js'; import { @@ -210,9 +210,7 @@ export function css_props(payload, is_html, props, component, dynamic = false) { */ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { if (styles) { - attrs.style = attrs.style - ? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles)) - : style_object_to_string(styles); + attrs.style = to_style(attrs.style, styles); } if (attrs.class) { @@ -286,35 +284,23 @@ function style_object_to_string(style_object) { .join(' '); } -/** @param {Record} style_object */ -export function add_styles(style_object) { - const styles = style_object_to_string(style_object); - return styles ? ` style="${styles}"` : ''; +/** + * @param {any} value + * @param {string | undefined} [hash] + * @param {Record} [directives] + */ +export function attr_class(value, hash, directives) { + var result = to_class(value, hash, directives); + return result ? ` class="${escape_html(result, true)}"` : ''; } /** - * @param {string} attribute - * @param {Record} styles + * @param {any} value + * @param {Record|[Record,Record]} [directives] */ -export function merge_styles(attribute, styles) { - /** @type {Record} */ - var merged = {}; - - if (attribute) { - for (var declaration of attribute.split(';')) { - var i = declaration.indexOf(':'); - var name = declaration.slice(0, i).trim(); - var value = declaration.slice(i + 1).trim(); - - if (name !== '') merged[name] = value; - } - } - - for (name in styles) { - merged[name] = styles[name]; - } - - return merged; +export function attr_style(value, directives) { + var result = to_style(value, directives); + return result ? ` style="${escape_html(result, true)}"` : ''; } /** @@ -549,7 +535,7 @@ export function props_id(payload) { return uid; } -export { attr, clsx, to_class }; +export { attr, clsx }; export { html } from './blocks/html.js'; diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index 89cc17e51b..c8758c1d4d 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -22,7 +22,7 @@ const replacements = { * @returns {string} */ export function attr(name, value, is_boolean = false) { - if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return ''; + if (value == null || (!value && is_boolean)) return ''; const normalized = (name in replacements && replacements[name].get(value)) || value; const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; return ` ${name}${assignment}`; @@ -82,3 +82,138 @@ export function to_class(value, hash, directives) { return classname === '' ? null : classname; } + +/** + * + * @param {Record} styles + * @param {boolean} important + */ +function append_styles(styles, important = false) { + var separator = important ? ' !important;' : ';'; + var css = ''; + + for (var key in styles) { + var value = styles[key]; + if (value != null && value !== '') { + css += ' ' + key + ': ' + value + separator; + } + } + + return css; +} + +/** + * @param {string} name + * @returns {string} + */ +function to_css_name(name) { + if (name[0] !== '-' || name[1] !== '-') { + return name.toLowerCase(); + } + return name; +} + +/** + * @param {any} value + * @param {Record | [Record, Record]} [styles] + * @returns {string | null} + */ +export function to_style(value, styles) { + if (styles) { + var new_style = ''; + + /** @type {Record | undefined} */ + var normal_styles; + + /** @type {Record | undefined} */ + var important_styles; + + if (Array.isArray(styles)) { + normal_styles = styles[0]; + important_styles = styles[1]; + } else { + normal_styles = styles; + } + + if (value) { + value = String(value) + .replaceAll(/\s*\/\*.*?\*\/\s*/g, '') + .trim(); + + /** @type {boolean | '"' | "'"} */ + var in_str = false; + var in_apo = 0; + var in_comment = false; + + var reserved_names = []; + + if (normal_styles) { + reserved_names.push(...Object.keys(normal_styles).map(to_css_name)); + } + if (important_styles) { + reserved_names.push(...Object.keys(important_styles).map(to_css_name)); + } + + var start_index = 0; + var name_index = -1; + + const len = value.length; + for (var i = 0; i < len; i++) { + var c = value[i]; + + if (in_comment) { + if (c === '/' && value[i - 1] === '*') { + in_comment = false; + } + } else if (in_str) { + if (in_str === c) { + in_str = false; + } + } else if (c === '/' && value[i + 1] === '*') { + in_comment = true; + } else if (c === '"' || c === "'") { + in_str = c; + } else if (c === '(') { + in_apo++; + } else if (c === ')') { + in_apo--; + } + + if (!in_comment && in_str === false && in_apo === 0) { + if (c === ':' && name_index === -1) { + name_index = i; + } else if (c === ';' || i === len - 1) { + if (name_index !== -1) { + var name = to_css_name(value.substring(start_index, name_index).trim()); + + if (!reserved_names.includes(name)) { + if (c !== ';') { + i++; + } + + var property = value.substring(start_index, i).trim(); + new_style += ' ' + property + ';'; + } + } + + start_index = i + 1; + name_index = -1; + } + } + } + } + + if (normal_styles) { + new_style += append_styles(normal_styles); + } + + if (important_styles) { + new_style += append_styles(important_styles, true); + } + + new_style = new_style.trim(); + return new_style === '' ? null : new_style; + } + + return value == null ? null : String(value); +} diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js b/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js index 9ff0007c37..04c9868ac3 100644 --- a/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-directive-spread-and-attr-empty/_config.js @@ -2,6 +2,6 @@ import { test } from '../../test'; export default test({ html: ` -

+

` }); diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js index adcdc4706d..e9965b2b1e 100644 --- a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/_config.js @@ -2,7 +2,7 @@ import { ok, test } from '../../test'; export default test({ html: ` -

color: red

+

color: red;

`, test({ assert, component, target, window }) { diff --git a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte index 35b768547e..e07adaa1c9 100644 --- a/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/inline-style-optimisation-bailout/main.svelte @@ -1,5 +1,5 @@

{styles}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js index f682972179..20092ddadf 100644 --- a/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-style-attr/_config.js @@ -2,7 +2,7 @@ import { test } from '../../test'; import { flushSync } from 'svelte'; export default test({ - html: `
Hello world

diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js new file mode 100644 index 0000000000..bd76e4e6b9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/_config.js @@ -0,0 +1,95 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +// This test counts mutations on hydration +// set_style() should not mutate style on hydration, except if mismatch +export default test({ + mode: ['server', 'hydrate'], + + server_props: { + browser: false + }, + + props: { + browser: true + }, + + ssrHtml: ` +
+
+
+
+
+
+
+
+
+
+
+ `, + + html: ` +
+
+
+
+
+
+
+
+
+
+
+ `, + + async test({ target, assert, component, instance }) { + flushSync(); + tick(); + assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']); + + let divs = target.querySelectorAll('div'); + + // Note : we cannot compare HTML because set_style() use dom.style.cssText + // which can alter the format of the attribute... + + divs.forEach((d) => assert.equal(d.style.margin, '')); + divs.forEach((d) => assert.equal(d.style.color, 'red')); + divs.forEach((d) => assert.equal(d.style.fontSize, '18px')); + + component.margin = '1px'; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'], + 'margin' + ); + divs.forEach((d) => assert.equal(d.style.margin, '1px')); + + component.color = 'yellow'; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'], + 'color' + ); + divs.forEach((d) => assert.equal(d.style.color, 'yellow')); + + component.fontSize = '10px'; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'], + 'fontSize' + ); + divs.forEach((d) => assert.equal(d.style.fontSize, '10px')); + + component.fontSize = null; + flushSync(); + assert.deepEqual( + instance.get_and_clear_mutations(), + ['DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV'], + 'fontSize' + ); + divs.forEach((d) => assert.equal(d.style.fontSize, '')); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte new file mode 100644 index 0000000000..ae4da8ae37 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-mutations/main.svelte @@ -0,0 +1,54 @@ + + +
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/style-update/_config.js b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js index f0b7f2648e..52690a431a 100644 --- a/packages/svelte/tests/runtime-runes/samples/style-update/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/style-update/_config.js @@ -3,6 +3,7 @@ import { test } from '../../test'; const style_1 = 'invalid-key:0; margin:4px;;color: green ;color:blue '; const style_2 = ' other-key : 0 ; padding:2px; COLOR:green; color: blue'; +const style_2_normalized = 'padding: 2px; color: blue;'; // https://github.com/sveltejs/svelte/issues/15309 export default test({ @@ -10,7 +11,7 @@ export default test({ style: style_1 }, - html: ` + ssrHtml: `
@@ -25,11 +26,11 @@ export default test({ assert.htmlEqual( target.innerHTML, ` -
-
+
+
- - + + ` ); From ae615ae2acb0a5574547b9161390a2761f60d2d4 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Thu, 6 Mar 2025 02:52:34 +0100 Subject: [PATCH 030/145] chore: memoize clsx() (alternative) (#15456) * memoize clsx + directives * changeset * unused * tweak * tweak changeset --------- Co-authored-by: Rich Harris --- .changeset/flat-jars-search.md | 5 +++++ .../3-transform/client/visitors/RegularElement.js | 15 +++++++-------- .../3-transform/client/visitors/shared/element.js | 14 +++++++------- .../src/internal/client/dom/elements/class.js | 2 +- 4 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 .changeset/flat-jars-search.md diff --git a/.changeset/flat-jars-search.md b/.changeset/flat-jars-search.md new file mode 100644 index 0000000000..fc0de76f95 --- /dev/null +++ b/.changeset/flat-jars-search.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: memoize `clsx` calls diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 6122dc4e0e..9b3ecc922d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -511,22 +511,21 @@ function setup_select_synchronization(value_binding, context) { /** * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context - * @return {ObjectExpression} + * @return {ObjectExpression | Identifier} */ export function build_class_directives_object(class_directives, context) { let properties = []; + let has_call_or_state = false; for (const d of class_directives) { - let expression = /** @type Expression */ (context.visit(d.expression)); - - if (d.metadata.expression.has_call) { - expression = get_expression_id(context.state, expression); - } - + const expression = /** @type Expression */ (context.visit(d.expression)); properties.push(b.init(d.name, expression)); + has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; } - return b.object(properties); + const directives = b.object(properties); + + return has_call_or_state ? get_expression_id(context.state, directives) : directives; } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index e0eb04d823..084c1e7c67 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -162,13 +162,13 @@ export function get_attribute_name(element, attribute) { * @param {boolean} is_html */ export function build_set_class(element, node_id, attribute, class_directives, context, is_html) { - let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? get_expression_id(context.state, value) : value - ); + let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => { + if (attribute.metadata.needs_clsx) { + value = b.call('$.clsx', value); + } - if (attribute && attribute.metadata.needs_clsx) { - value = b.call('$.clsx', value); - } + return metadata.has_call ? get_expression_id(context.state, value) : value; + }); /** @type {Identifier | undefined} */ let previous_id; @@ -176,7 +176,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c /** @type {ObjectExpression | Identifier | undefined} */ let prev; - /** @type {ObjectExpression | undefined} */ + /** @type {ObjectExpression | Identifier | undefined} */ let next; if (class_directives.length) { diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js index 7027c84f62..ecbfcbc010 100644 --- a/packages/svelte/src/internal/client/dom/elements/class.js +++ b/packages/svelte/src/internal/client/dom/elements/class.js @@ -33,7 +33,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes) // @ts-expect-error need to add __className to patched prototype dom.__className = value; - } else if (next_classes) { + } else if (next_classes && prev_classes !== next_classes) { for (var key in next_classes) { var is_present = !!next_classes[key]; From d513304dd063f8d01d470b9a4a476cdb24d9d20c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:55:52 -0500 Subject: [PATCH 031/145] Version Packages (#15453) Co-authored-by: github-actions[bot] --- .changeset/flat-jars-search.md | 5 ----- .changeset/quiet-baboons-listen.md | 5 ----- .changeset/real-cameras-attack.md | 5 ----- .changeset/strange-planes-shout.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/flat-jars-search.md delete mode 100644 .changeset/quiet-baboons-listen.md delete mode 100644 .changeset/real-cameras-attack.md delete mode 100644 .changeset/strange-planes-shout.md diff --git a/.changeset/flat-jars-search.md b/.changeset/flat-jars-search.md deleted file mode 100644 index fc0de76f95..0000000000 --- a/.changeset/flat-jars-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: memoize `clsx` calls diff --git a/.changeset/quiet-baboons-listen.md b/.changeset/quiet-baboons-listen.md deleted file mode 100644 index eb5b4cc699..0000000000 --- a/.changeset/quiet-baboons-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: respect `svelte-ignore hydration_attribute_changed` on elements with spread attributes diff --git a/.changeset/real-cameras-attack.md b/.changeset/real-cameras-attack.md deleted file mode 100644 index 35e2764785..0000000000 --- a/.changeset/real-cameras-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always use `setAttribute` when setting `style` diff --git a/.changeset/strange-planes-shout.md b/.changeset/strange-planes-shout.md deleted file mode 100644 index 58ef252740..0000000000 --- a/.changeset/strange-planes-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: make `style:` directive and CSS handling more robust diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 0ff6b62fe0..44f1f7cb79 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.22.5 + +### Patch Changes + +- fix: memoize `clsx` calls ([#15456](https://github.com/sveltejs/svelte/pull/15456)) + +- fix: respect `svelte-ignore hydration_attribute_changed` on elements with spread attributes ([#15443](https://github.com/sveltejs/svelte/pull/15443)) + +- fix: always use `setAttribute` when setting `style` ([#15323](https://github.com/sveltejs/svelte/pull/15323)) + +- fix: make `style:` directive and CSS handling more robust ([#15418](https://github.com/sveltejs/svelte/pull/15418)) + ## 5.22.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1f95811cd5..fb20167a4e 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.22.4", + "version": "5.22.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 845375314f..e20a9683dd 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.22.4'; +export const VERSION = '5.22.5'; export const PUBLIC_VERSION = '5'; From 2c4d85bcec74f78a6291b07920c423908714aefc Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 5 Mar 2025 18:49:52 -0800 Subject: [PATCH 032/145] docs: address `$effect` feedback (#15107) * docs: address $effect feedback * also add a note to the migration guide * minor wording tweak * update onMount docs * Update documentation/docs/02-runes/05-$effect.md Co-authored-by: Rich Harris * restore order * soften a bit * add back mention of updating template in response to effects * define parent effect * state that they don't run on the server * Update documentation/docs/02-runes/04-$effect.md Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * format * Apply suggestions from code review Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * update onMount docs * add 'Understanding lifecycle' section * note * tweak wording --------- Co-authored-by: Rich Harris Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- documentation/docs/02-runes/04-$effect.md | 30 +++++++++++-------- .../docs/07-misc/07-v5-migration-guide.md | 2 ++ packages/svelte/src/index-client.js | 11 +++---- packages/svelte/types/index.d.ts | 11 +++---- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index da24084d4d..e346bceba8 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -2,15 +2,11 @@ title: $effect --- -Effects are what make your application _do things_. When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes. +Effects are functions that run when state updates, and can be used for things like calling third-party libraries, drawing on `` elements, or making network requests. They only run in the browser, not during server-side rendering. -Most of the effects in a Svelte app are created by Svelte itself — they're the bits that update the text in `

hello {name}!

` when `name` changes, for example. +Generally speaking, you should _not_ update state inside effects, as it will make code more convoluted and will often lead to never-ending update cycles. If you find yourself doing so, see [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches. -But you can also create your own effects with the `$effect` rune, which is useful when you need to synchronize an external system (whether that's a library, or a `` element, or something across a network) with state inside your Svelte app. - -> [!NOTE] Avoid overusing `$effect`! When you do too much work in effects, code often becomes difficult to understand and maintain. See [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches. - -Your effects run after the component has been mounted to the DOM, and in a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) after state changes ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)): +You can create an effect with the `$effect` rune ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)): ```svelte ``` +Note that [when `$effect` runs is different]($effect#Understanding-dependencies) than when `$:` runs. + > [!DETAILS] Why we did this > `$:` was a great shorthand and easy to get started with: you could slap a `$:` in front of most code and it would somehow work. This intuitiveness was also its drawback the more complicated your code became, because it wasn't as easy to reason about. Was the intent of the code to create a derivation, or a side effect? With `$derived` and `$effect`, you have a bit more up-front decision making to do (spoiler alert: 90% of the time you want `$derived`), but future-you and other developers on your team will have an easier time. > diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index efcf7b727b..fd8e999da7 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -45,13 +45,14 @@ if (DEV) { } /** - * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. - * It must be called during the component's initialisation (but doesn't need to live *inside* the component; - * it can be called from an external module). + * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. + * Unlike `$effect`, the provided function only runs once. * - * If a function is returned _synchronously_ from `onMount`, it will be called when the component is unmounted. + * It must be called during the component's initialisation (but doesn't need to live _inside_ the component; + * it can be called from an external module). If a function is returned _synchronously_ from `onMount`, + * it will be called when the component is unmounted. * - * `onMount` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). + * `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render). * * @template T * @param {() => NotFunction | Promise> | (() => any)} fn diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c3dbdcac79..c6000fc4b6 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -349,13 +349,14 @@ declare module 'svelte' { props: Props; }); /** - * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. - * It must be called during the component's initialisation (but doesn't need to live *inside* the component; - * it can be called from an external module). + * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. + * Unlike `$effect`, the provided function only runs once. * - * If a function is returned _synchronously_ from `onMount`, it will be called when the component is unmounted. + * It must be called during the component's initialisation (but doesn't need to live _inside_ the component; + * it can be called from an external module). If a function is returned _synchronously_ from `onMount`, + * it will be called when the component is unmounted. * - * `onMount` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). + * `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render). * * */ export function onMount(fn: () => NotFunction | Promise> | (() => any)): void; From c5912aad71e77e75e4dcec8c27acb49e412f92f2 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Fri, 7 Mar 2025 15:30:56 +0100 Subject: [PATCH 033/145] fix: null and warnings for local handlers (#15460) * fix null and warning for local handlers * test * changeset * treat `let handler = () => {...}` the same as `function handler() {...}` --------- Co-authored-by: Rich Harris --- .changeset/shy-mirrors-remain.md | 5 ++ .../client/visitors/shared/events.js | 26 +++++++--- packages/svelte/src/compiler/phases/scope.js | 15 ++++++ .../internal/client/dom/elements/events.js | 11 ++--- .../event-handler-invalid-values/_config.js | 48 +++++++++++++++++++ .../event-handler-invalid-values/main.svelte | 10 ++++ 6 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 .changeset/shy-mirrors-remain.md create mode 100644 packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte diff --git a/.changeset/shy-mirrors-remain.md b/.changeset/shy-mirrors-remain.md new file mode 100644 index 0000000000..028f7beb68 --- /dev/null +++ b/.changeset/shy-mirrors-remain.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: null and warnings for local handlers diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index f23f7548ec..2667a96f6a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -46,8 +46,12 @@ export function visit_event_attribute(node, context) { // When we hoist a function we assign an array with the function and all // hoisted closure params. - const args = [handler, ...hoisted_params]; - delegated_assignment = b.array(args); + if (hoisted_params) { + const args = [handler, ...hoisted_params]; + delegated_assignment = b.array(args); + } else { + delegated_assignment = handler; + } } else { delegated_assignment = handler; } @@ -123,11 +127,19 @@ export function build_event_handler(node, metadata, context) { } // function declared in the script - if ( - handler.type === 'Identifier' && - context.state.scope.get(handler.name)?.declaration_kind !== 'import' - ) { - return handler; + if (handler.type === 'Identifier') { + const binding = context.state.scope.get(handler.name); + + if (binding?.is_function()) { + return handler; + } + + // local variable can be assigned directly + // except in dev mode where when need $.apply() + // in order to handle warnings. + if (!dev && binding?.declaration_kind !== 'import') { + return handler; + } } if (metadata.has_call) { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 7d9f90982a..a5227c1b51 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -79,6 +79,21 @@ export class Binding { get updated() { return this.mutated || this.reassigned; } + + is_function() { + if (this.reassigned) { + // even if it's reassigned to another function, + // we can't use it directly as e.g. an event handler + return false; + } + + if (this.declaration_kind === 'function') { + return true; + } + + const type = this.initial?.type; + return type === 'ArrowFunctionExpression' || type === 'FunctionExpression'; + } } export class Scope { diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 25ece5f569..0c1bb1dada 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -238,7 +238,7 @@ export function handle_event_propagation(event) { var delegated = current_target['__' + event_name]; if ( - delegated !== undefined && + delegated != null && (!(/** @type {any} */ (current_target).disabled) || // DOM could've been updated already by the time this is reached, so we check this as well // -> the target could not have been disabled because it emits the event in the first place @@ -311,13 +311,11 @@ export function apply( error = e; } - if (typeof handler === 'function') { - handler.apply(element, args); - } else if (has_side_effects || handler != null || error) { + if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) { const filename = component?.[FILENAME]; const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`; - - const event_name = args[0].type; + const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : ''; + const event_name = args[0]?.type + phase; const description = `\`${event_name}\` handler${location}`; const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`'; @@ -327,4 +325,5 @@ export function apply( throw error; } } + handler?.apply(element, args); } diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js new file mode 100644 index 0000000000..d53812d4c3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js @@ -0,0 +1,48 @@ +import { assertType } from 'vitest'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + compileOptions: { + dev: true + }, + + test({ assert, target, warnings, logs }) { + /** @type {any} */ + let error = null; + + const handler = (/** @type {any} */ e) => { + error = e.error; + e.stopImmediatePropagation(); + }; + + window.addEventListener('error', handler, true); + + const [b1, b2, b3] = target.querySelectorAll('button'); + + b1.click(); + assert.deepEqual(logs, []); + assert.equal(error, null); + + error = null; + logs.length = 0; + + b2.click(); + assert.deepEqual(logs, ['clicked']); + assert.equal(error, null); + + error = null; + logs.length = 0; + + b3.click(); + assert.deepEqual(logs, []); + assert.deepEqual(warnings, [ + '`click` handler at main.svelte:10:17 should be a function. Did you mean to add a leading `() =>`?' + ]); + assert.isNotNull(error); + assert.match(error.message, /is not a function/); + + window.removeEventListener('error', handler, true); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte new file mode 100644 index 0000000000..f6e344ece8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte @@ -0,0 +1,10 @@ + + + + + From 3326bd8ae77a43b6ae2b03970969066e716f6063 Mon Sep 17 00:00:00 2001 From: "Trevor N. Suarez" Date: Fri, 7 Mar 2025 08:02:02 -0700 Subject: [PATCH 034/145] feat: Add `closedby` to `HTMLDialogAttributes` (dialog element) (#15458) * Adding the `closedby` attribute to dialog element Spec: https://html.spec.whatwg.org/#attr-dialog-closedby * Adding changeset * Update .changeset/metal-spoons-scream.md --------- Co-authored-by: Rich Harris --- .changeset/metal-spoons-scream.md | 5 +++++ packages/svelte/elements.d.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/metal-spoons-scream.md diff --git a/.changeset/metal-spoons-scream.md b/.changeset/metal-spoons-scream.md new file mode 100644 index 0000000000..2eb7b7140c --- /dev/null +++ b/.changeset/metal-spoons-scream.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: Add `closedby` property to HTMLDialogAttributes type diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 6d256b5620..08687cafaf 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -957,6 +957,7 @@ export interface HTMLDelAttributes extends HTMLAttributes { export interface HTMLDialogAttributes extends HTMLAttributes { open?: boolean | undefined | null; + closedby?: 'any' | 'closerequest' | 'none' | undefined | null; } export interface HTMLEmbedAttributes extends HTMLAttributes { From eaf0087d7c5671794ee25521667860b1a8af1828 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 7 Mar 2025 10:02:58 -0500 Subject: [PATCH 035/145] fix: skip `log_if_contains_state` if only logging literals (#15468) --- .changeset/hungry-monkeys-fly.md | 5 +++++ .../phases/3-transform/client/visitors/CallExpression.js | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/hungry-monkeys-fly.md diff --git a/.changeset/hungry-monkeys-fly.md b/.changeset/hungry-monkeys-fly.md new file mode 100644 index 0000000000..f52c8dad92 --- /dev/null +++ b/.changeset/hungry-monkeys-fly.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip `log_if_contains_state` if only logging literals diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 7a3057451a..fda43ad791 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -44,7 +44,8 @@ export function CallExpression(node, context) { node.callee.property.type === 'Identifier' && ['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes( node.callee.property.name - ) + ) && + node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases? ) { return b.call( node.callee, From e2bbc560e434df055402eb018789754a41afc456 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 10:07:50 -0500 Subject: [PATCH 036/145] Version Packages (#15466) Co-authored-by: github-actions[bot] --- .changeset/hungry-monkeys-fly.md | 5 ----- .changeset/metal-spoons-scream.md | 5 ----- .changeset/shy-mirrors-remain.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/hungry-monkeys-fly.md delete mode 100644 .changeset/metal-spoons-scream.md delete mode 100644 .changeset/shy-mirrors-remain.md diff --git a/.changeset/hungry-monkeys-fly.md b/.changeset/hungry-monkeys-fly.md deleted file mode 100644 index f52c8dad92..0000000000 --- a/.changeset/hungry-monkeys-fly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: skip `log_if_contains_state` if only logging literals diff --git a/.changeset/metal-spoons-scream.md b/.changeset/metal-spoons-scream.md deleted file mode 100644 index 2eb7b7140c..0000000000 --- a/.changeset/metal-spoons-scream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: Add `closedby` property to HTMLDialogAttributes type diff --git a/.changeset/shy-mirrors-remain.md b/.changeset/shy-mirrors-remain.md deleted file mode 100644 index 028f7beb68..0000000000 --- a/.changeset/shy-mirrors-remain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: null and warnings for local handlers diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 44f1f7cb79..a05938dacc 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.22.6 + +### Patch Changes + +- fix: skip `log_if_contains_state` if only logging literals ([#15468](https://github.com/sveltejs/svelte/pull/15468)) + +- fix: Add `closedby` property to HTMLDialogAttributes type ([#15458](https://github.com/sveltejs/svelte/pull/15458)) + +- fix: null and warnings for local handlers ([#15460](https://github.com/sveltejs/svelte/pull/15460)) + ## 5.22.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index fb20167a4e..53f03e3543 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.22.5", + "version": "5.22.6", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e20a9683dd..01e1b390a3 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.22.5'; +export const VERSION = '5.22.6'; export const PUBLIC_VERSION = '5'; From 1c0e24013fff52e106870dfc7e4b38a817c25610 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 10 Mar 2025 11:22:21 -0400 Subject: [PATCH 037/145] chore: reuse is_function helper (#15467) --- .../phases/2-analyze/visitors/Attribute.js | 12 ++---------- packages/svelte/src/compiler/phases/scope.js | 18 +++++++++++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 561a004526..9124a8822f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -162,16 +162,8 @@ function get_delegated_event(event_name, handler, context) { return unhoisted; } - if (binding !== null && binding.initial !== null && !binding.updated) { - const binding_type = binding.initial.type; - - if ( - binding_type === 'ArrowFunctionExpression' || - binding_type === 'FunctionDeclaration' || - binding_type === 'FunctionExpression' - ) { - target_function = binding.initial; - } + if (binding?.is_function()) { + target_function = binding.initial; } } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index a5227c1b51..b6063c3234 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,4 +1,4 @@ -/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -80,19 +80,23 @@ export class Binding { return this.mutated || this.reassigned; } + /** + * @returns {this is Binding & { initial: ArrowFunctionExpression | FunctionDeclaration | FunctionExpression }} + */ is_function() { - if (this.reassigned) { + if (this.updated) { // even if it's reassigned to another function, // we can't use it directly as e.g. an event handler return false; } - if (this.declaration_kind === 'function') { - return true; - } - const type = this.initial?.type; - return type === 'ArrowFunctionExpression' || type === 'FunctionExpression'; + + return ( + type === 'ArrowFunctionExpression' || + type === 'FunctionExpression' || + type === 'FunctionDeclaration' + ); } } From 81480c40a02678bd02d89e2e20d50a6ddd3ec383 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:48:50 -0700 Subject: [PATCH 038/145] chore: add missing permissions for `pkg.pr.new-comment` (#15489) --- .github/workflows/pkg.pr.new-comment.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml index b1fba0b04b..3f1fca5a0b 100644 --- a/.github/workflows/pkg.pr.new-comment.yml +++ b/.github/workflows/pkg.pr.new-comment.yml @@ -6,6 +6,9 @@ on: types: - completed +permissions: + pull-requests: write + jobs: build: name: 'Update comment' From dbd4617ac42a4bf5e97af05f2d5bc6a0517e24b2 Mon Sep 17 00:00:00 2001 From: Maple <52185471+fuuki12@users.noreply.github.com> Date: Tue, 11 Mar 2025 06:52:22 +0900 Subject: [PATCH 039/145] docs: correct toggle function in lifecycle hooks example (#15486) --- documentation/docs/06-runtime/03-lifecycle-hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md index 2b97ca796f..f051c46d73 100644 --- a/documentation/docs/06-runtime/03-lifecycle-hooks.md +++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md @@ -147,7 +147,7 @@ With runes, we can use `$effect.pre`, which behaves the same as `$effect` but ru } function toggle() { - toggleValue = !toggleValue; + theme = theme === 'dark' ? 'light' : 'dark'; } From 1cc5bcdc999716673d15844bd1190758f882ba05 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 11 Mar 2025 18:57:58 +0100 Subject: [PATCH 040/145] chore: clarify fuzzyset adaption (#15491) it was BSD in 2016 but has undergone some license changes since then - clarify in the comment that the adaption was from the 2016 BSD version --- .../svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js index cd72d73005..28b314cdd5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js @@ -12,8 +12,8 @@ export default function fuzzymatch(name, names) { return matches && matches[0][0] > 0.7 ? matches[0][1] : null; } -// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js -// BSD Licensed +// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js in 2016 +// BSD Licensed (see https://github.com/Glench/fuzzyset.js/issues/10) const GRAM_SIZE_LOWER = 2; const GRAM_SIZE_UPPER = 3; From 110d42062fb1a98698d2a68a39919cc44962613d Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 11 Mar 2025 18:48:55 +0000 Subject: [PATCH 041/145] fix: on teardown, use the last known value for the signal before the set (#15469) * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * Update packages/svelte/src/internal/client/reactivity/props.js Co-authored-by: Rich Harris * Update packages/svelte/src/internal/client/reactivity/props.js Co-authored-by: Rich Harris * Update packages/svelte/src/internal/client/reactivity/props.js Co-authored-by: Rich Harris * lint * lint * lint * Update .changeset/sharp-elephants-invite.md --------- Co-authored-by: Rich Harris --- .changeset/sharp-elephants-invite.md | 5 ++ .../2-analyze/visitors/CallExpression.js | 3 + .../svelte/src/internal/client/context.js | 11 ++- .../src/internal/client/reactivity/props.js | 43 +++++++----- .../src/internal/client/reactivity/sources.js | 11 ++- .../svelte/src/internal/client/runtime.js | 7 +- .../svelte/src/internal/client/types.d.ts | 2 + .../ondestroy-prop-access-2/Component.svelte | 11 +++ .../ondestroy-prop-access-2/_config.js | 14 ++++ .../ondestroy-prop-access-2/main.svelte | 15 ++++ .../ondestroy-prop-access-3/Component.svelte | 5 ++ .../ondestroy-prop-access-3/_config.js | 11 +++ .../ondestroy-prop-access-3/main.svelte | 16 +++++ .../ondestroy-prop-access/Component.svelte | 12 ++++ .../samples/ondestroy-prop-access/_config.js | 68 +++++++++++++++++++ .../samples/ondestroy-prop-access/main.svelte | 41 +++++++++++ .../samples/nested-effect-conflict/_config.js | 10 +-- 17 files changed, 254 insertions(+), 31 deletions(-) create mode 100644 .changeset/sharp-elephants-invite.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte diff --git a/.changeset/sharp-elephants-invite.md b/.changeset/sharp-elephants-invite.md new file mode 100644 index 0000000000..3a106cd450 --- /dev/null +++ b/.changeset/sharp-elephants-invite.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +fix: make values consistent between effects and their cleanup functions diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 4d09d9293f..6c21717852 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -42,6 +42,9 @@ export function CallExpression(node, context) { e.bindable_invalid_location(node); } + // We need context in case the bound prop is stale + context.state.analysis.needs_context = true; + break; case '$host': diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index bd94d5ad8a..bfca9d5e6a 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -11,7 +11,7 @@ import { set_active_reaction, untrack } from './runtime.js'; -import { effect } from './reactivity/effects.js'; +import { effect, teardown } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; /** @type {ComponentContext | null} */ @@ -112,15 +112,16 @@ export function getAllContexts() { * @returns {void} */ export function push(props, runes = false, fn) { - component_context = { + var ctx = (component_context = { p: component_context, c: null, + d: false, e: null, m: false, s: props, x: null, l: null - }; + }); if (legacy_mode_flag && !runes) { component_context.l = { @@ -131,6 +132,10 @@ export function push(props, runes = false, fn) { }; } + teardown(() => { + /** @type {ComponentContext} */ (ctx).d = true; + }); + if (DEV) { // component function component_context.function = fn; diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 5a3b30281f..bd85b14df0 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -1,4 +1,4 @@ -/** @import { Source } from './types.js' */ +/** @import { Derived, Source } from './types.js' */ import { DEV } from 'esm-env'; import { PROPS_IS_BINDABLE, @@ -10,24 +10,10 @@ import { import { get_descriptor, is_function } from '../../shared/utils.js'; import { mutable_source, set, source, update } from './sources.js'; import { derived, derived_safe_equal } from './deriveds.js'; -import { - active_effect, - get, - captured_signals, - set_active_effect, - untrack, - active_reaction, - set_active_reaction -} from '../runtime.js'; +import { get, captured_signals, untrack } from '../runtime.js'; import { safe_equals } from './equality.js'; import * as e from '../errors.js'; -import { - BRANCH_EFFECT, - LEGACY_DERIVED_PROP, - LEGACY_PROPS, - ROOT_EFFECT, - STATE_SYMBOL -} from '../constants.js'; +import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '../constants.js'; import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; @@ -249,6 +235,14 @@ export function spread_props(...props) { return new Proxy({ props }, spread_props_handler); } +/** + * @param {Derived} current_value + * @returns {boolean} + */ +function has_destroyed_component_ctx(current_value) { + return current_value.ctx?.d ?? false; +} + /** * This function is responsible for synchronizing a possibly bound prop with the inner component state. * It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value. @@ -382,6 +376,11 @@ export function prop(props, key, flags, fallback) { return (inner_current_value.v = parent_value); }); + // Ensure we eagerly capture the initial value if it's bindable + if (bindable) { + get(current_value); + } + if (!immutable) current_value.equals = safe_equals; return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { @@ -408,11 +407,21 @@ export function prop(props, key, flags, fallback) { if (fallback_used && fallback_value !== undefined) { fallback_value = new_value; } + + if (has_destroyed_component_ctx(current_value)) { + return value; + } + untrack(() => get(current_value)); // force a synchronisation immediately } return value; } + + if (has_destroyed_component_ctx(current_value)) { + return current_value.v; + } + return get(current_value); }; } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index f6a3fd7e33..49584e8626 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -14,7 +14,8 @@ import { derived_sources, set_derived_sources, check_dirtiness, - untracking + untracking, + is_destroying_effect } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -34,6 +35,7 @@ import { get_stack } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; export let inspect_effects = new Set(); +export const old_values = new Map(); /** * @param {Set} v @@ -168,6 +170,13 @@ export function set(source, value) { export function internal_set(source, value) { if (!source.equals(value)) { var old_value = source.v; + + if (is_destroying_effect) { + old_values.set(source, value); + } else { + old_values.set(source, old_value); + } + source.v = value; source.wv = increment_write_version(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index bbe4dc3d9b..aa0a41e71f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -25,7 +25,7 @@ import { BOUNDARY_EFFECT } from './constants.js'; import { flush_tasks } from './dom/task.js'; -import { internal_set } from './reactivity/sources.js'; +import { internal_set, old_values } from './reactivity/sources.js'; import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { FILENAME } from '../../constants.js'; @@ -673,6 +673,7 @@ function flush_queued_root_effects() { if (DEV) { dev_effect_stack = []; } + old_values.clear(); } } @@ -923,6 +924,10 @@ export function get(signal) { } } + if (is_destroying_effect && old_values.has(signal)) { + return old_values.get(signal); + } + return signal.v; } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 7208ed7783..0c260a0a9f 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -14,6 +14,8 @@ export type ComponentContext = { p: null | ComponentContext; /** context */ c: null | Map; + /** destroyed */ + d: boolean; /** deferred effects */ e: null | Array<{ fn: () => void | (() => void); diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte new file mode 100644 index 0000000000..73347c4d7f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte @@ -0,0 +1,11 @@ + + +{my_prop.foo} diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js new file mode 100644 index 0000000000..81005cf737 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js @@ -0,0 +1,14 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, logs }) { + const [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.deepEqual(logs, ['bar']); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte new file mode 100644 index 0000000000..f38b37fb7f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte @@ -0,0 +1,15 @@ + + + + +{#if value !== undefined} + +{/if} diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte new file mode 100644 index 0000000000..5bfb777128 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js new file mode 100644 index 0000000000..0eb68310cb --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ target }) { + const [btn1] = target.querySelectorAll('button'); + + btn1.click(); + flushSync(); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte new file mode 100644 index 0000000000..9c72d2c48a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte @@ -0,0 +1,16 @@ + + +{#if state} + {@const attributes = { title: state.title }} + +{/if} + diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte new file mode 100644 index 0000000000..761f303c2e --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte @@ -0,0 +1,12 @@ + + +

{count}

+ + diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js new file mode 100644 index 0000000000..2ffb7e653f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js @@ -0,0 +1,68 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, logs }) { + const [btn1, btn2, btn3] = target.querySelectorAll('button'); + let ps = [...target.querySelectorAll('p')]; + + for (const p of ps) { + assert.equal(p.innerHTML, '0'); + } + + flushSync(() => { + btn1.click(); + }); + + // prop update normally if we are not unmounting + for (const p of ps) { + assert.equal(p.innerHTML, '1'); + } + + flushSync(() => { + btn3.click(); + }); + + // binding still works and update the value correctly + for (const p of ps) { + assert.equal(p.innerHTML, '0'); + } + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + console.warn(logs); + + // the five components guarded by `count < 2` unmount and log + assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]); + + flushSync(() => { + btn2.click(); + }); + + // the three components guarded by `show` unmount and log + assert.deepEqual(logs, [ + 1, + true, + 1, + true, + 1, + true, + 1, + true, + 1, + true, + 2, + true, + 2, + true, + 2, + true + ]); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte new file mode 100644 index 0000000000..73a7501e9d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte @@ -0,0 +1,41 @@ + + + + + + +{#if count < 2} + +{/if} + + +{#if count < 2} + +{/if} + + +{#if count < 2} + +{/if} + + +{#if show} + +{/if} + + + + + + + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js b/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js index a8c16b7008..eb631bc9f4 100644 --- a/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js @@ -10,14 +10,6 @@ export default test({ }); await Promise.resolve(); - assert.deepEqual(logs, [ - 'top level', - 'inner', - 0, - 'destroy inner', - undefined, - 'destroy outer', - undefined - ]); + assert.deepEqual(logs, ['top level', 'inner', 0, 'destroy inner', 0, 'destroy outer', 0]); } }); From a1257c17f5ba93bdbf7c470a5720ff9a69a224dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:51:01 -0400 Subject: [PATCH 042/145] Version Packages (#15493) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/sharp-elephants-invite.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/sharp-elephants-invite.md diff --git a/.changeset/sharp-elephants-invite.md b/.changeset/sharp-elephants-invite.md deleted file mode 100644 index 3a106cd450..0000000000 --- a/.changeset/sharp-elephants-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -fix: make values consistent between effects and their cleanup functions diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index a05938dacc..65b3edd1fd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.23.0 + +### Minor Changes + +- fix: make values consistent between effects and their cleanup functions ([#15469](https://github.com/sveltejs/svelte/pull/15469)) + ## 5.22.6 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 53f03e3543..b3ac0a5b51 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.22.6", + "version": "5.23.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 01e1b390a3..5f06fd0753 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.22.6'; +export const VERSION = '5.23.0'; export const PUBLIC_VERSION = '5'; From b27ca425c792d53d58346d76850add7db1cef3ad Mon Sep 17 00:00:00 2001 From: "D.V. Colomban" <17250935+dvcol@users.noreply.github.com> Date: Wed, 12 Mar 2025 23:58:40 +0100 Subject: [PATCH 043/145] fix: add files and group properties to HTMLInputAttributes (#15492) Fixes #14579 Although this isn't 100% correct because there's no `group` attribute, there's no better way to make people have components' props just be `HTMLInputAttributes` and allow them to reference `group` --- .changeset/plenty-bats-lay.md | 5 +++++ packages/svelte/elements.d.ts | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/plenty-bats-lay.md diff --git a/.changeset/plenty-bats-lay.md b/.changeset/plenty-bats-lay.md new file mode 100644 index 0000000000..cd5ce66e42 --- /dev/null +++ b/.changeset/plenty-bats-lay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 08687cafaf..99d87b4c09 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1076,6 +1076,7 @@ export interface HTMLInputAttributes extends HTMLAttributes { checked?: boolean | undefined | null; dirname?: string | undefined | null; disabled?: boolean | undefined | null; + files?: FileList | undefined | null; form?: string | undefined | null; formaction?: string | undefined | null; formenctype?: @@ -1087,6 +1088,7 @@ export interface HTMLInputAttributes extends HTMLAttributes { formmethod?: 'dialog' | 'get' | 'post' | 'DIALOG' | 'GET' | 'POST' | undefined | null; formnovalidate?: boolean | undefined | null; formtarget?: string | undefined | null; + group?: any | undefined | null; height?: number | string | undefined | null; indeterminate?: boolean | undefined | null; list?: string | undefined | null; From 5d3aa2bda4bea7af39607a4a01ce8f0eff6cd56b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 13 Mar 2025 19:49:26 +0000 Subject: [PATCH 044/145] fix: ensure transient writes to tracked parent effects works as expected (#15506) * ix: ensure transient writes to tracked parent effects works as expected * lint * format test * tweak changeset --------- Co-authored-by: Rich Harris --- .changeset/brown-rockets-shake.md | 5 +++++ packages/svelte/src/internal/client/runtime.js | 8 ++++++++ .../samples/untracked-write-pre/_config.js | 7 +++++++ .../samples/untracked-write-pre/main.svelte | 13 +++++++++++++ 4 files changed, 33 insertions(+) create mode 100644 .changeset/brown-rockets-shake.md create mode 100644 packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte diff --git a/.changeset/brown-rockets-shake.md b/.changeset/brown-rockets-shake.md new file mode 100644 index 0000000000..3772a88f6e --- /dev/null +++ b/.changeset/brown-rockets-shake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: invalidate parent effects when child effects update parent dependencies diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index aa0a41e71f..0a65c6e45a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -460,6 +460,14 @@ export function update_reaction(reaction) { // the same version if (previous_reaction !== null) { read_version++; + + if (untracked_writes !== null) { + if (previous_untracked_writes === null) { + previous_untracked_writes = untracked_writes; + } else { + previous_untracked_writes.push(.../** @type {Source[]} */ (untracked_writes)); + } + } } return result; diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js new file mode 100644 index 0000000000..0310ec4fbb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + assert.deepEqual(logs, ['Outer', 'Inner', 'Outer', 'Inner']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte new file mode 100644 index 0000000000..5e95dbfd41 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte @@ -0,0 +1,13 @@ + From 489f463d7bf80da22c92b686e507eb7c0dc41967 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 14 Mar 2025 03:26:45 -0700 Subject: [PATCH 045/145] fix: replace `undefined` with `void 0` to avoid edge case (#15511) * replace 'undefined' with 'void 0' * lint * YALF * reuse expression * oops --------- Co-authored-by: Rich Harris --- .changeset/curvy-countries-flow.md | 5 +++++ .../3-transform/client/visitors/VariableDeclaration.js | 3 +-- .../phases/3-transform/server/visitors/CallExpression.js | 4 ++-- .../3-transform/server/visitors/VariableDeclaration.js | 5 ++--- packages/svelte/src/compiler/utils/builders.js | 2 ++ 5 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 .changeset/curvy-countries-flow.md diff --git a/.changeset/curvy-countries-flow.md b/.changeset/curvy-countries-flow.md new file mode 100644 index 0000000000..6ef8545804 --- /dev/null +++ b/.changeset/curvy-countries-flow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: replace `undefined` with `void 0` to avoid edge case diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 31e712cdcc..baffc5dec3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -116,8 +116,7 @@ export function VariableDeclaration(node, context) { } const args = /** @type {CallExpression} */ (init).arguments; - const value = - args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0])); + const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; if (rune === '$state' || rune === '$state.raw') { /** diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 386c6b6ff3..a425bc5ec4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -13,11 +13,11 @@ export function CallExpression(node, context) { const rune = get_rune(node, context.state.scope); if (rune === '$host') { - return b.id('undefined'); + return b.void0; } if (rune === '$effect.tracking') { - return b.literal(false); + return b.false; } if (rune === '$effect.root') { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index c4c31d7eb3..a9c9777335 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -45,7 +45,7 @@ export function VariableDeclaration(node, context) { ) { const right = node.right.arguments.length ? /** @type {Expression} */ (context.visit(node.right.arguments[0])) - : b.id('undefined'); + : b.void0; return b.assignment_pattern(node.left, right); } } @@ -75,8 +75,7 @@ export function VariableDeclaration(node, context) { } const args = /** @type {CallExpression} */ (init).arguments; - const value = - args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0])); + const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; if (rune === '$derived.by') { declarations.push( diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index ecb595d74d..4ec2930cc2 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -154,6 +154,8 @@ export function unary(operator, argument) { return { type: 'UnaryExpression', argument, operator, prefix: true }; } +export const void0 = unary('void', literal(0)); + /** * @param {ESTree.Expression} test * @param {ESTree.Expression} consequent From dab1a1b467b8c5e85964d74c378357bea00e5fe9 Mon Sep 17 00:00:00 2001 From: Scott Wu Date: Fri, 14 Mar 2025 19:11:08 +0800 Subject: [PATCH 046/145] docs: Update 99-faq.md (#15510) --- documentation/docs/07-misc/99-faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md index b56c27af86..7e25cdab55 100644 --- a/documentation/docs/07-misc/99-faq.md +++ b/documentation/docs/07-misc/99-faq.md @@ -46,7 +46,7 @@ It will show up on hover. - You can use markdown here. - You can also use code blocks here. - Usage: - ```tsx + ```svelte
``` --> From 18d71fd5288c04d5574f5abe18e7c990053c5876 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 07:12:02 -0400 Subject: [PATCH 047/145] chore: reuse expression nodes (#15513) --- .../compiler/phases/3-transform/client/transform-client.js | 2 +- .../phases/3-transform/client/visitors/AnimateDirective.js | 2 +- .../compiler/phases/3-transform/client/visitors/AwaitBlock.js | 2 +- .../phases/3-transform/client/visitors/BinaryExpression.js | 4 ++-- .../compiler/phases/3-transform/client/visitors/IfBlock.js | 4 +--- .../phases/3-transform/client/visitors/RegularElement.js | 2 +- .../phases/3-transform/client/visitors/SlotElement.js | 2 +- .../phases/3-transform/client/visitors/shared/element.js | 4 ++-- .../phases/3-transform/server/visitors/SlotElement.js | 2 +- packages/svelte/src/compiler/utils/builders.js | 4 ++-- 10 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index cf5ba285cb..ac8263b916 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -596,7 +596,7 @@ export function client_component(analysis, options) { /** @type {ESTree.Property[]} */ ( [ prop_def.attribute ? b.init('attribute', b.literal(prop_def.attribute)) : undefined, - prop_def.reflect ? b.init('reflect', b.literal(true)) : undefined, + prop_def.reflect ? b.init('reflect', b.true) : undefined, prop_def.type ? b.init('type', b.literal(prop_def.type)) : undefined ].filter(Boolean) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js index 510f32cde5..2e051ec674 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js @@ -11,7 +11,7 @@ import { parse_directive_name } from './shared/utils.js'; export function AnimateDirective(node, context) { const expression = node.expression === null - ? b.literal(null) + ? b.null : b.thunk(/** @type {Expression} */ (context.visit(node.expression))); // in after_update to ensure it always happens after bind:this diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index e0aef2d316..7588b24280 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -60,7 +60,7 @@ export function AwaitBlock(node, context) { expression, node.pending ? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending))) - : b.literal(null), + : b.null, then_block, catch_block ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js index c8c54a5a59..c563920855 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js @@ -16,7 +16,7 @@ export function BinaryExpression(node, context) { '$.strict_equals', /** @type {Expression} */ (context.visit(node.left)), /** @type {Expression} */ (context.visit(node.right)), - operator === '!==' && b.literal(false) + operator === '!==' && b.false ); } @@ -25,7 +25,7 @@ export function BinaryExpression(node, context) { '$.equals', /** @type {Expression} */ (context.visit(node.left)), /** @type {Expression} */ (context.visit(node.right)), - operator === '!=' && b.literal(false) + operator === '!=' && b.false ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index 0876fa30b6..fdd21b2b7e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -40,9 +40,7 @@ export function IfBlock(node, context) { b.if( /** @type {Expression} */ (context.visit(node.test)), b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), - alternate_id - ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.literal(false))) - : undefined + alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined ) ]) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 9b3ecc922d..45a594af1f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -689,7 +689,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont '=', b.member(node_id, 'value'), b.conditional( - b.binary('==', b.literal(null), b.assignment('=', b.member(node_id, '__value'), value)), + b.binary('==', b.null, b.assignment('=', b.member(node_id, '__value'), value)), b.literal(''), // render null/undefined values as empty string to support placeholder options value ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index fdd705e32e..c6f4ba1ed3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -59,7 +59,7 @@ export function SlotElement(node, context) { const fallback = node.fragment.nodes.length === 0 - ? b.literal(null) + ? b.null : b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))); const slot = b.call( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 084c1e7c67..97cec7a729 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -95,7 +95,7 @@ export function build_set_attributes( const call = b.call( '$.set_attributes', element_id, - is_dynamic ? attributes_id : b.literal(null), + is_dynamic ? attributes_id : b.null, b.object(values), element.metadata.scoped && context.state.analysis.css.hash !== '' && @@ -120,7 +120,7 @@ export function build_set_attributes( */ export function build_attribute_value(value, context, memoize = (value) => value) { if (value === true) { - return { value: b.literal(true), has_state: false }; + return { value: b.true, has_state: false }; } if (!Array.isArray(value) || value.length === 1) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js index 7ece04ae3d..e7925071cd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js @@ -38,7 +38,7 @@ export function SlotElement(node, context) { const fallback = node.fragment.nodes.length === 0 - ? b.literal(null) + ? b.null : b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment))); const slot = b.call( diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 4ec2930cc2..736738d19f 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -485,7 +485,7 @@ export function do_while(test, body) { const true_instance = literal(true); const false_instance = literal(false); -const null_instane = literal(null); +const null_instance = literal(null); /** @type {ESTree.DebuggerStatement} */ const debugger_builder = { @@ -647,7 +647,7 @@ export { return_builder as return, if_builder as if, this_instance as this, - null_instane as null, + null_instance as null, debugger_builder as debugger }; From e74fbcbbacc529bbb7ff8cc7d6b8b5d75d647cfa Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:19:24 -0700 Subject: [PATCH 048/145] chore: don't distribute unused types definitions (#15473) hopefully helps with #15182, also makes the package smaller --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/hungry-dancers-tap.md | 5 +++++ packages/svelte/package.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/hungry-dancers-tap.md diff --git a/.changeset/hungry-dancers-tap.md b/.changeset/hungry-dancers-tap.md new file mode 100644 index 0000000000..51b2f86019 --- /dev/null +++ b/.changeset/hungry-dancers-tap.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: don't distribute unused types definitions diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b3ac0a5b51..c74c9d34ca 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -9,11 +9,12 @@ "node": ">=18" }, "files": [ + "*.d.ts", "src", "!src/**/*.test.*", + "!src/**/*.d.ts", "types", "compiler", - "*.d.ts", "README.md" ], "module": "src/index-client.js", From f227cfcea86b46e6a8ee389484d855c306ed66eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=CC=81ric=20NICOLAS?= Date: Fri, 14 Mar 2025 22:30:49 +0100 Subject: [PATCH 049/145] fix: Allow global-like pseudo-selectors refinement (#15313) For instance, specifying a tree-structural pseudo-class to `::view-transition-new` should still constitute a valid global-like selector. Fixes #15312 --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/gold-hairs-jog.md | 5 +++++ .../src/compiler/phases/2-analyze/css/css-analyze.js | 8 +++++++- .../svelte/tests/css/samples/view-transition/expected.css | 6 ++++++ .../svelte/tests/css/samples/view-transition/input.svelte | 6 ++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/gold-hairs-jog.md diff --git a/.changeset/gold-hairs-jog.md b/.changeset/gold-hairs-jog.md new file mode 100644 index 0000000000..eaafced314 --- /dev/null +++ b/.changeset/gold-hairs-jog.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow global-like pseudo-selectors refinement diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index ed22838582..362ac9dcad 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -133,7 +133,13 @@ const css_visitors = { node.metadata.is_global = node.selectors.length >= 1 && is_global(node); - if (node.selectors.length === 1) { + if ( + node.selectors.length >= 1 && + node.selectors.every( + (selector) => + selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' + ) + ) { const first = node.selectors[0]; node.metadata.is_global_like ||= (first.type === 'PseudoClassSelector' && first.name === 'host') || diff --git a/packages/svelte/tests/css/samples/view-transition/expected.css b/packages/svelte/tests/css/samples/view-transition/expected.css index afc84d52eb..e216a4d3ad 100644 --- a/packages/svelte/tests/css/samples/view-transition/expected.css +++ b/packages/svelte/tests/css/samples/view-transition/expected.css @@ -8,9 +8,15 @@ ::view-transition-old { animation-duration: 0.5s; } + ::view-transition-old:only-child { + animation-duration: 0.5s; + } ::view-transition-new { animation-duration: 0.5s; } + ::view-transition-new:only-child { + animation-duration: 0.5s; + } ::view-transition-image-pair { animation-duration: 0.5s; } diff --git a/packages/svelte/tests/css/samples/view-transition/input.svelte b/packages/svelte/tests/css/samples/view-transition/input.svelte index ebb2b3fd88..345213ccd3 100644 --- a/packages/svelte/tests/css/samples/view-transition/input.svelte +++ b/packages/svelte/tests/css/samples/view-transition/input.svelte @@ -8,9 +8,15 @@ ::view-transition-old { animation-duration: 0.5s; } + ::view-transition-old:only-child { + animation-duration: 0.5s; + } ::view-transition-new { animation-duration: 0.5s; } + ::view-transition-new:only-child { + animation-duration: 0.5s; + } ::view-transition-image-pair { animation-duration: 0.5s; } From 8e9a21e374c3bafb79e046e64ef4d625f466749f Mon Sep 17 00:00:00 2001 From: 7nik Date: Fri, 14 Mar 2025 23:44:28 +0200 Subject: [PATCH 050/145] fix: correctly match `:has()`'s selector during css pruning (#15277) Fixes #14072 `:has()` was matching only against descendants or siblings, but not sibling's descendants. This makes the logic be able to go forward or backwards, simplifying a lot of cases along the way. --- .changeset/cuddly-chefs-refuse.md | 5 + .../phases/2-analyze/css/css-prune.js | 427 +++++++++--------- .../svelte/tests/css/samples/has/_config.js | 148 +++--- .../svelte/tests/css/samples/has/expected.css | 13 + .../svelte/tests/css/samples/has/input.svelte | 21 + .../css/samples/render-tag-loop/_config.js | 17 +- .../css/samples/render-tag-loop/expected.css | 9 +- .../css/samples/render-tag-loop/input.svelte | 10 +- 8 files changed, 353 insertions(+), 297 deletions(-) create mode 100644 .changeset/cuddly-chefs-refuse.md diff --git a/.changeset/cuddly-chefs-refuse.md b/.changeset/cuddly-chefs-refuse.md new file mode 100644 index 0000000000..6672ac4ab3 --- /dev/null +++ b/.changeset/cuddly-chefs-refuse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly match `:has()` selector during css pruning diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index fc8108e46e..070ec7cd34 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -5,9 +5,12 @@ import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../ import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ +/** @typedef {FORWARD | BACKWARD} Direction */ const NODE_PROBABLY_EXISTS = 0; const NODE_DEFINITELY_EXISTS = 1; +const FORWARD = 0; +const BACKWARD = 1; const whitelist_attribute_selector = new Map([ ['details', ['open']], @@ -43,6 +46,27 @@ const nesting_selector = { } }; +/** @type {Compiler.AST.CSS.RelativeSelector} */ +const any_selector = { + type: 'RelativeSelector', + start: -1, + end: -1, + combinator: null, + selectors: [ + { + type: 'TypeSelector', + name: '*', + start: -1, + end: -1 + } + ], + metadata: { + is_global: false, + is_global_like: false, + scoped: false + } +}; + /** * Snippets encountered already (avoids infinite loops) * @type {Set} @@ -72,7 +96,8 @@ export function prune(stylesheet, element) { apply_selector( selectors, /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule), - element + element, + BACKWARD ) ) { node.metadata.used = true; @@ -159,16 +184,17 @@ function truncate(node) { * @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {Direction} direction * @returns {boolean} */ -function apply_selector(relative_selectors, rule, element) { - const parent_selectors = relative_selectors.slice(); - const relative_selector = parent_selectors.pop(); +function apply_selector(relative_selectors, rule, element, direction) { + const rest_selectors = relative_selectors.slice(); + const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop(); const matched = !!relative_selector && - relative_selector_might_apply_to_node(relative_selector, rule, element) && - apply_combinator(relative_selector, parent_selectors, rule, element); + relative_selector_might_apply_to_node(relative_selector, rule, element, direction) && + apply_combinator(relative_selector, rest_selectors, rule, element, direction); if (matched) { if (!is_outer_global(relative_selector)) { @@ -183,76 +209,63 @@ function apply_selector(relative_selectors, rule, element) { /** * @param {Compiler.AST.CSS.RelativeSelector} relative_selector - * @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors + * @param {Compiler.AST.CSS.RelativeSelector[]} rest_selectors * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node + * @param {Direction} direction * @returns {boolean} */ -function apply_combinator(relative_selector, parent_selectors, rule, node) { - if (!relative_selector.combinator) return true; +function apply_combinator(relative_selector, rest_selectors, rule, node, direction) { + const combinator = + direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator; + if (!combinator) return true; - const name = relative_selector.combinator.name; - - switch (name) { + switch (combinator.name) { case ' ': case '>': { + const is_adjacent = combinator.name === '>'; + const parents = + direction === FORWARD + ? get_descendant_elements(node, is_adjacent) + : get_ancestor_elements(node, is_adjacent); let parent_matched = false; - const path = node.metadata.path; - let i = path.length; - - while (i--) { - const parent = path[i]; - - if (parent.type === 'SnippetBlock') { - if (seen.has(parent)) { - parent_matched = true; - } else { - seen.add(parent); - - for (const site of parent.metadata.sites) { - if (apply_combinator(relative_selector, parent_selectors, rule, site)) { - parent_matched = true; - } - } - } - - break; - } - - if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { - if (apply_selector(parent_selectors, rule, parent)) { - parent_matched = true; - } - - if (name === '>') return parent_matched; + for (const parent of parents) { + if (apply_selector(rest_selectors, rule, parent, direction)) { + parent_matched = true; } } - return parent_matched || parent_selectors.every((selector) => is_global(selector, rule)); + return ( + parent_matched || + (direction === BACKWARD && + (!is_adjacent || parents.length === 0) && + rest_selectors.every((selector) => is_global(selector, rule))) + ); } case '+': case '~': { - const siblings = get_possible_element_siblings(node, name === '+'); + const siblings = get_possible_element_siblings(node, direction, combinator.name === '+'); let sibling_matched = false; for (const possible_sibling of siblings.keys()) { if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { // `{@render foo()}

foo

` with `:global(.x) + p` is a match - if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { + if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) { sibling_matched = true; } - } else if (apply_selector(parent_selectors, rule, possible_sibling)) { + } else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) { sibling_matched = true; } } return ( sibling_matched || - (get_element_parent(node) === null && - parent_selectors.every((selector) => is_global(selector, rule))) + (direction === BACKWARD && + get_element_parent(node) === null && + rest_selectors.every((selector) => is_global(selector, rule))) ); } @@ -313,9 +326,10 @@ const regex_backslash_and_following_character = /\\(.)/g; * @param {Compiler.AST.CSS.RelativeSelector} relative_selector * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {Direction} direction * @returns {boolean} */ -function relative_selector_might_apply_to_node(relative_selector, rule, element) { +function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) { // Sort :has(...) selectors in one bucket and everything else into another const has_selectors = []; const other_selectors = []; @@ -331,13 +345,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) // If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match. // In that case ignore this check (because we just came from this) to avoid an infinite loop. if (has_selectors.length > 0) { - /** @type {Array} */ - const child_elements = []; - /** @type {Array} */ - const descendant_elements = []; - /** @type {Array} */ - let sibling_elements; // do them lazy because it's rarely used and expensive to calculate - // If this is a :has inside a global selector, we gotta include the element itself, too, // because the global selector might be for an element that's outside the component, // e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} } @@ -353,46 +360,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) ) ) ); - if (include_self) { - child_elements.push(element); - descendant_elements.push(element); - } - - const seen = new Set(); - - /** - * @param {Compiler.AST.SvelteNode} node - * @param {{ is_child: boolean }} state - */ - function walk_children(node, state) { - walk(node, state, { - _(node, context) { - if (node.type === 'RegularElement' || node.type === 'SvelteElement') { - descendant_elements.push(node); - - if (context.state.is_child) { - child_elements.push(node); - context.state.is_child = false; - context.next(); - context.state.is_child = true; - } else { - context.next(); - } - } else if (node.type === 'RenderTag') { - for (const snippet of node.metadata.snippets) { - if (seen.has(snippet)) continue; - - seen.add(snippet); - walk_children(snippet.body, context.state); - } - } else { - context.next(); - } - } - }); - } - - walk_children(element.fragment, { is_child: true }); // :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the @@ -403,37 +370,34 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) let matched = false; for (const complex_selector of complex_selectors) { - const selectors = truncate(complex_selector); - const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator; - // In .x:has(> y), we want to search for y, ignoring the left-most combinator - // (else it would try to walk further up and fail because there are no selectors left) - if (selectors.length > 0) { - selectors[0] = { - ...selectors[0], - combinator: null - }; + const [first, ...rest] = truncate(complex_selector); + // if it was just a :global(...) + if (!first) { + complex_selector.metadata.used = true; + matched = true; + continue; } - const descendants = - left_most_combinator.name === '+' || left_most_combinator.name === '~' - ? (sibling_elements ??= get_following_sibling_elements(element, include_self)) - : left_most_combinator.name === '>' - ? child_elements - : descendant_elements; - - let selector_matched = false; - - // Iterate over all descendant elements and check if the selector inside :has matches - for (const element of descendants) { - if ( - selectors.length === 0 /* is :global(...) */ || - (element.metadata.scoped && selector_matched) || - apply_selector(selectors, rule, element) - ) { + if (include_self) { + const selector_including_self = [ + first.combinator ? { ...first, combinator: null } : first, + ...rest + ]; + if (apply_selector(selector_including_self, rule, element, FORWARD)) { complex_selector.metadata.used = true; - selector_matched = matched = true; + matched = true; } } + + const selector_excluding_self = [ + any_selector, + first.combinator ? first : { ...first, combinator: descendant_combinator }, + ...rest + ]; + if (apply_selector(selector_excluding_self, rule, element, FORWARD)) { + complex_selector.metadata.used = true; + matched = true; + } } if (!matched) { @@ -458,7 +422,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) ) { const args = selector.args; const complex_selector = args.children[0]; - return apply_selector(complex_selector.children, rule, element); + return apply_selector(complex_selector.children, rule, element, BACKWARD); } // We came across a :global, everything beyond it is global and therefore a potential match @@ -507,7 +471,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) if (is_global) { complex_selector.metadata.used = true; matched = true; - } else if (apply_selector(relative, rule, element)) { + } else if (apply_selector(relative, rule, element, BACKWARD)) { complex_selector.metadata.used = true; matched = true; } else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) { @@ -591,7 +555,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) for (const complex_selector of parent.prelude.children) { if ( - apply_selector(get_relative_selectors(complex_selector), parent, element) || + apply_selector(get_relative_selectors(complex_selector), parent, element, direction) || complex_selector.children.every((s) => is_global(s, parent)) ) { complex_selector.metadata.used = true; @@ -612,80 +576,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) return true; } -/** - * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element - * @param {boolean} include_self - */ -function get_following_sibling_elements(element, include_self) { - const path = element.metadata.path; - let i = path.length; - - /** @type {Compiler.AST.SvelteNode} */ - let start = element; - let nodes = /** @type {Compiler.AST.SvelteNode[]} */ ( - /** @type {Compiler.AST.Fragment} */ (path[0]).nodes - ); - - // find the set of nodes to walk... - while (i--) { - const node = path[i]; - - if (node.type === 'RegularElement' || node.type === 'SvelteElement') { - nodes = node.fragment.nodes; - break; - } - - if (node.type !== 'Fragment') { - start = node; - } - } - - /** @type {Array} */ - const siblings = []; - - // ...then walk them, starting from the node containing the element in question - // skipping nodes that appears before the element - - const seen = new Set(); - let skip = true; - - /** @param {Compiler.AST.SvelteNode} node */ - function get_siblings(node) { - walk(node, null, { - RegularElement(node) { - if (node === element) { - skip = false; - if (include_self) siblings.push(node); - } else if (!skip) { - siblings.push(node); - } - }, - SvelteElement(node) { - if (node === element) { - skip = false; - if (include_self) siblings.push(node); - } else if (!skip) { - siblings.push(node); - } - }, - RenderTag(node) { - for (const snippet of node.metadata.snippets) { - if (seen.has(snippet)) continue; - - seen.add(snippet); - get_siblings(snippet.body); - } - } - }); - } - - for (const node of nodes.slice(nodes.indexOf(start))) { - get_siblings(node); - } - - return siblings; -} - /** * @param {any} operator * @param {any} expected_value @@ -822,6 +712,84 @@ function unquote(str) { return str; } +/** + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node + * @param {boolean} adjacent_only + * @param {Set} seen + */ +function get_ancestor_elements(node, adjacent_only, seen = new Set()) { + /** @type {Array} */ + const ancestors = []; + + const path = node.metadata.path; + let i = path.length; + + while (i--) { + const parent = path[i]; + + if (parent.type === 'SnippetBlock') { + if (!seen.has(parent)) { + seen.add(parent); + + for (const site of parent.metadata.sites) { + ancestors.push(...get_ancestor_elements(site, adjacent_only, seen)); + } + } + + break; + } + + if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { + ancestors.push(parent); + if (adjacent_only) { + break; + } + } + } + + return ancestors; +} + +/** + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node + * @param {boolean} adjacent_only + * @param {Set} seen + */ +function get_descendant_elements(node, adjacent_only, seen = new Set()) { + /** @type {Array} */ + const descendants = []; + + /** + * @param {Compiler.AST.SvelteNode} node + */ + function walk_children(node) { + walk(node, null, { + _(node, context) { + if (node.type === 'RegularElement' || node.type === 'SvelteElement') { + descendants.push(node); + + if (!adjacent_only) { + context.next(); + } + } else if (node.type === 'RenderTag') { + for (const snippet of node.metadata.snippets) { + if (seen.has(snippet)) continue; + + seen.add(snippet); + walk_children(snippet.body); + } + } else { + context.next(); + } + } + }); + } + + walk_children(node.type === 'RenderTag' ? node : node.fragment); + + return descendants; +} + /** * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node * @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} @@ -843,11 +811,12 @@ function get_element_parent(node) { /** * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node + * @param {Direction} direction * @param {boolean} adjacent_only * @param {Set} seen * @returns {Map} */ -function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { +function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) { /** @type {Map} */ const result = new Map(); const path = node.metadata.path; @@ -859,9 +828,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { while (i--) { const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]); - let j = fragment.nodes.indexOf(current); + let j = fragment.nodes.indexOf(current) + (direction === FORWARD ? 1 : -1); - while (j--) { + while (j >= 0 && j < fragment.nodes.length) { const node = fragment.nodes[j]; if (node.type === 'RegularElement') { @@ -876,21 +845,28 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { return result; } } + // Special case: slots, render tags and svelte:element tags could resolve to no siblings, + // so we want to continue until we find a definite sibling even with the adjacent-only combinator } else if (is_block(node)) { if (node.type === 'SlotElement') { result.set(node, NODE_PROBABLY_EXISTS); } - const possible_last_child = get_possible_last_child(node, adjacent_only); + const possible_last_child = get_possible_nested_siblings(node, direction, adjacent_only); add_to_map(possible_last_child, result); if (adjacent_only && has_definite_elements(possible_last_child)) { return result; } - } else if (node.type === 'RenderTag' || node.type === 'SvelteElement') { + } else if (node.type === 'SvelteElement') { result.set(node, NODE_PROBABLY_EXISTS); - // Special case: slots, render tags and svelte:element tags could resolve to no siblings, - // so we want to continue until we find a definite sibling even with the adjacent-only combinator + } else if (node.type === 'RenderTag') { + result.set(node, NODE_PROBABLY_EXISTS); + for (const snippet of node.metadata.snippets) { + add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only), result); + } } + + j = direction === FORWARD ? j + 1 : j - 1; } current = path[i]; @@ -910,7 +886,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { seen.add(current); for (const site of current.metadata.sites) { - const siblings = get_possible_element_siblings(site, adjacent_only, seen); + const siblings = get_possible_element_siblings(site, direction, adjacent_only, seen); add_to_map(siblings, result); if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) { @@ -923,7 +899,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { if (current.type === 'EachBlock' && fragment === current.body) { // `{#each ...}{/each}` — `` can be previous sibling of `` - add_to_map(get_possible_last_child(current, adjacent_only), result); + add_to_map(get_possible_nested_siblings(current, direction, adjacent_only), result); } } @@ -931,11 +907,13 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { } /** - * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node + * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock} node + * @param {Direction} direction * @param {boolean} adjacent_only + * @param {Set} seen * @returns {Map} */ -function get_possible_last_child(node, adjacent_only) { +function get_possible_nested_siblings(node, direction, adjacent_only, seen = new Set()) { /** @type {Array} */ let fragments = []; @@ -956,12 +934,20 @@ function get_possible_last_child(node, adjacent_only) { case 'SlotElement': fragments.push(node.fragment); break; + + case 'SnippetBlock': + if (seen.has(node)) { + return new Map(); + } + seen.add(node); + fragments.push(node.body); + break; } /** @type {Map} NodeMap */ const result = new Map(); - let exhaustive = node.type !== 'SlotElement'; + let exhaustive = node.type !== 'SlotElement' && node.type !== 'SnippetBlock'; for (const fragment of fragments) { if (fragment == null) { @@ -969,7 +955,7 @@ function get_possible_last_child(node, adjacent_only) { continue; } - const map = loop_child(fragment.nodes, adjacent_only); + const map = loop_child(fragment.nodes, direction, adjacent_only, seen); exhaustive &&= has_definite_elements(map); add_to_map(map, result); @@ -1012,27 +998,28 @@ function add_to_map(from, to) { } /** - * @param {NodeExistsValue | undefined} exist1 + * @param {NodeExistsValue} exist1 * @param {NodeExistsValue | undefined} exist2 * @returns {NodeExistsValue} */ function higher_existence(exist1, exist2) { - // @ts-expect-error TODO figure out if this is a bug - if (exist1 === undefined || exist2 === undefined) return exist1 || exist2; + if (exist2 === undefined) return exist1; return exist1 > exist2 ? exist1 : exist2; } /** * @param {Compiler.AST.SvelteNode[]} children + * @param {Direction} direction * @param {boolean} adjacent_only + * @param {Set} seen */ -function loop_child(children, adjacent_only) { +function loop_child(children, direction, adjacent_only, seen) { /** @type {Map} */ const result = new Map(); - let i = children.length; + let i = direction === FORWARD ? 0 : children.length - 1; - while (i--) { + while (i >= 0 && i < children.length) { const child = children[i]; if (child.type === 'RegularElement') { @@ -1042,13 +1029,19 @@ function loop_child(children, adjacent_only) { } } else if (child.type === 'SvelteElement') { result.set(child, NODE_PROBABLY_EXISTS); + } else if (child.type === 'RenderTag') { + for (const snippet of child.metadata.snippets) { + add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only, seen), result); + } } else if (is_block(child)) { - const child_result = get_possible_last_child(child, adjacent_only); + const child_result = get_possible_nested_siblings(child, direction, adjacent_only, seen); add_to_map(child_result, result); if (adjacent_only && has_definite_elements(child_result)) { break; } } + + i = direction === FORWARD ? i + 1 : i - 1; } return result; diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 8d89d98cbd..5700a09b96 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -6,210 +6,238 @@ export default test({ code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(y)"', start: { - line: 33, + line: 41, column: 1, - character: 330 + character: 378 }, end: { - line: 33, + line: 41, column: 15, - character: 344 + character: 392 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(:global(y))"', start: { - line: 36, + line: 44, column: 1, - character: 365 + character: 413 }, end: { - line: 36, + line: 44, column: 24, - character: 388 + character: 436 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(.unused)"', start: { - line: 39, + line: 47, column: 1, - character: 409 + character: 457 }, end: { - line: 39, + line: 47, column: 15, - character: 423 + character: 471 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo):has(.unused)"', start: { - line: 42, + line: 50, column: 1, - character: 444 + character: 492 }, end: { - line: 42, + line: 50, column: 27, - character: 470 + character: 518 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(y):has(.unused)"', start: { - line: 52, + line: 60, column: 1, - character: 578 + character: 626 }, end: { - line: 52, + line: 60, column: 22, - character: 599 + character: 647 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused"', start: { - line: 71, + line: 79, column: 2, - character: 804 + character: 852 }, end: { - line: 71, + line: 79, column: 9, - character: 811 + character: 859 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused x:has(y)"', start: { - line: 87, + line: 95, column: 1, - character: 958 + character: 1006 }, end: { - line: 87, + line: 95, column: 17, - character: 974 + character: 1022 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(.unused)"', start: { - line: 90, + line: 98, column: 1, - character: 995 + character: 1043 }, end: { - line: 90, + line: 98, column: 21, - character: 1015 + character: 1063 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(> z)"', start: { - line: 100, + line: 108, column: 1, - character: 1115 + character: 1163 }, end: { - line: 100, + line: 108, column: 11, - character: 1125 + character: 1173 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(> d)"', start: { - line: 103, + line: 111, column: 1, - character: 1146 + character: 1194 }, end: { - line: 103, + line: 111, column: 11, - character: 1156 + character: 1204 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(~ y)"', start: { - line: 123, + line: 131, column: 1, - character: 1348 + character: 1396 }, end: { - line: 123, + line: 131, column: 11, - character: 1358 + character: 1406 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "d:has(+ f)"', + start: { + line: 141, + column: 1, + character: 1494 + }, + end: { + line: 141, + column: 11, + character: 1504 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "f:has(~ d)"', start: { - line: 133, + line: 144, column: 1, - character: 1446 + character: 1525 }, end: { - line: 133, + line: 144, column: 11, - character: 1456 + character: 1535 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":has(.unused)"', start: { - line: 141, + line: 152, column: 2, - character: 1529 + character: 1608 }, end: { - line: 141, + line: 152, column: 15, - character: 1542 + character: 1621 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "&:has(.unused)"', start: { - line: 147, + line: 158, column: 2, - character: 1600 + character: 1679 }, end: { - line: 147, + line: 158, column: 16, - character: 1614 + character: 1693 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo):has(.unused)"', start: { - line: 155, + line: 166, column: 1, - character: 1684 + character: 1763 }, end: { - line: 155, + line: 166, column: 27, - character: 1710 + character: 1789 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "h:has(> h > i)"', + start: { + line: 173, + column: 1, + character: 1848 + }, + end: { + line: 173, + column: 15, + character: 1862 } } ] diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css index b257370d61..2ce4d2bec5 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -118,6 +118,9 @@ d.svelte-xyz:has(~ f:where(.svelte-xyz)) { color: green; } + /* (unused) d:has(+ f) { + color: red; + }*/ /* (unused) f:has(~ d) { color: red; }*/ @@ -143,3 +146,13 @@ /* (unused) :global(.foo):has(.unused) { color: red; }*/ + + g.svelte-xyz:has(> h:where(.svelte-xyz) > i:where(.svelte-xyz)) { + color: green; + } + /* (unused) h:has(> h > i) { + color: red; + }*/ + g.svelte-xyz:has(+ j:where(.svelte-xyz) > k:where(.svelte-xyz)) { + color: green; + } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte index 9b254996bf..033471bc16 100644 --- a/packages/svelte/tests/css/samples/has/input.svelte +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -9,6 +9,14 @@ + + + + + + + + diff --git a/packages/svelte/tests/css/samples/render-tag-loop/_config.js b/packages/svelte/tests/css/samples/render-tag-loop/_config.js index f623b92cc3..292c6c49ac 100644 --- a/packages/svelte/tests/css/samples/render-tag-loop/_config.js +++ b/packages/svelte/tests/css/samples/render-tag-loop/_config.js @@ -1,20 +1,5 @@ import { test } from '../../test'; export default test({ - warnings: [ - { - code: 'css_unused_selector', - message: 'Unused CSS selector "div + div"', - start: { - line: 19, - column: 1, - character: 185 - }, - end: { - line: 19, - column: 10, - character: 194 - } - } - ] + warnings: [] }); diff --git a/packages/svelte/tests/css/samples/render-tag-loop/expected.css b/packages/svelte/tests/css/samples/render-tag-loop/expected.css index 9ced15e964..3e449286c9 100644 --- a/packages/svelte/tests/css/samples/render-tag-loop/expected.css +++ b/packages/svelte/tests/css/samples/render-tag-loop/expected.css @@ -2,9 +2,12 @@ div.svelte-xyz div:where(.svelte-xyz) { color: green; } - /* (unused) div + div { - color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? *\/ - }*/ + div.svelte-xyz + div:where(.svelte-xyz) { + color: green; + } div.svelte-xyz:has(div:where(.svelte-xyz)) { color: green; } + span.svelte-xyz:has(~span:where(.svelte-xyz)) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte index ade8df5744..3c55261f18 100644 --- a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte +++ b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte @@ -12,14 +12,22 @@ {/snippet} +{#snippet c()} + + {@render c()} +{/snippet} + From aaeda65f2f31585a4e48b452b73874e10fd4ebfc Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 14 Mar 2025 22:46:52 +0100 Subject: [PATCH 051/145] docs: add docs on state_unsafe_mutation error (#14932) closes #14752 --- .../98-reference/.generated/client-errors.md | 24 +++++++++++++++++++ .../svelte/messages/client-errors/errors.md | 24 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 2c2e0707ea..0beb3cb9a9 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -133,3 +133,27 @@ Reading state that was created inside the same derived is forbidden. Consider us ``` Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` ``` + +This error is thrown in a situation like this: + +```svelte + + + +``` + +Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. + +To fix this: +- See if it's possible to refactor your `$derived` such that the update becomes unnecessary +- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update +- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index ce1f222c63..ab4d1519c1 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -87,3 +87,27 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long ## state_unsafe_mutation > Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` + +This error is thrown in a situation like this: + +```svelte + + + +``` + +Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. + +To fix this: +- See if it's possible to refactor your `$derived` such that the update becomes unnecessary +- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update +- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates From 32ee6c1bc252023e79acb9c6c255079964dfb665 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Sat, 15 Mar 2025 18:46:44 +0100 Subject: [PATCH 052/145] rune_invalid_arguments_length (#15516) --- .changeset/two-spies-lie.md | 5 +++++ .../compiler/phases/2-analyze/visitors/CallExpression.js | 2 +- .../samples/runes-wrong-state-raw-args/_config.js | 8 ++++++++ .../samples/runes-wrong-state-raw-args/main.svelte | 3 +++ .../samples/runes-wrong-state-raw-args/main.svelte.js | 1 + 5 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .changeset/two-spies-lie.md create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js diff --git a/.changeset/two-spies-lie.md b/.changeset/two-spies-lie.md new file mode 100644 index 0000000000..2ea7fd6136 --- /dev/null +++ b/.changeset/two-spies-lie.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 6c21717852..6ef323725b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -117,7 +117,7 @@ export function CallExpression(node, context) { if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) { e.rune_invalid_arguments_length(node, rune, 'exactly one argument'); - } else if (rune === '$state' && node.arguments.length > 1) { + } else if (node.arguments.length > 1) { e.rune_invalid_arguments_length(node, rune, 'zero or one arguments'); } diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js new file mode 100644 index 0000000000..af226559d1 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'rune_invalid_arguments_length', + message: '`$state.raw` must be called with zero or one arguments' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte new file mode 100644 index 0000000000..2b50b43b9a --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js new file mode 100644 index 0000000000..442aaad142 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js @@ -0,0 +1 @@ +const foo = $state.raw(1, 2, 3); From f30d75ab7e289f741379de7d715c0cb2508a3596 Mon Sep 17 00:00:00 2001 From: Garik Asplund <111464359+garikAsplund@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:25:52 -0700 Subject: [PATCH 053/145] =?UTF-8?q?updated=20->=20to=20=20=E2=86=92=20in?= =?UTF-8?q?=20v5-migration-guide=20(#15526)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/07-misc/07-v5-migration-guide.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md index 87ff40cf47..36e9776364 100644 --- a/documentation/docs/07-misc/07-v5-migration-guide.md +++ b/documentation/docs/07-misc/07-v5-migration-guide.md @@ -10,7 +10,7 @@ You don't have to migrate to the new syntax right away - Svelte 5 still supports At the heart of Svelte 5 is the new runes API. Runes are basically compiler instructions that inform Svelte about reactivity. Syntactically, runes are functions starting with a dollar-sign. -### let -> $state +### let → $state In Svelte 4, a `let` declaration at the top level of a component was implicitly reactive. In Svelte 5, things are more explicit: a variable is reactive when created using the `$state` rune. Let's migrate the counter to runes mode by wrapping the counter in `$state`: @@ -25,7 +25,7 @@ Nothing else changes. `count` is still the number itself, and you read and write > [!DETAILS] Why we did this > `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more. -### $: -> $derived/$effect +### $: → $derived/$effect In Svelte 4, a `$:` statement at the top level of a component could be used to declare a derivation, i.e. state that is entirely defined through a computation of other state. In Svelte 5, this is achieved using the `$derived` rune: @@ -73,7 +73,7 @@ Note that [when `$effect` runs is different]($effect#Understanding-dependencies) > - executing dependencies as needed and therefore being immune to ordering problems > - being TypeScript-friendly -### export let -> $props +### export let → $props In Svelte 4, properties of a component were declared using `export let`. Each property was one declaration. In Svelte 5, all properties are declared through the `$props` rune, through destructuring: @@ -466,11 +466,11 @@ By now you should have a pretty good understanding of the before/after and how t We thought the same, which is why we provide a migration script to do most of the migration automatically. You can upgrade your project by using `npx sv migrate svelte-5`. This will do the following things: - bump core dependencies in your `package.json` -- migrate to runes (`let` -> `$state` etc) -- migrate to event attributes for DOM elements (`on:click` -> `onclick`) -- migrate slot creations to render tags (`` -> `{@render children()}`) -- migrate slot usages to snippets (`
...
` -> `{#snippet x()}
...
{/snippet}`) -- migrate obvious component creations (`new Component(...)` -> `mount(Component, ...)`) +- migrate to runes (`let` → `$state` etc) +- migrate to event attributes for DOM elements (`on:click` → `onclick`) +- migrate slot creations to render tags (`` → `{@render children()}`) +- migrate slot usages to snippets (`
...
` → `{#snippet x()}
...
{/snippet}`) +- migrate obvious component creations (`new Component(...)` → `mount(Component, ...)`) You can also migrate a single component in VS Code through the `Migrate Component to Svelte 5 Syntax` command, or in our Playground through the `Migrate` button. From e5881eade3e53316ee4329349cc1297c79d8522d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 16 Mar 2025 17:32:22 -0400 Subject: [PATCH 054/145] chore: tweak migration doc diff blocks (#15527) --- .../docs/07-misc/07-v5-migration-guide.md | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md index 36e9776364..e502b7921a 100644 --- a/documentation/docs/07-misc/07-v5-migration-guide.md +++ b/documentation/docs/07-misc/07-v5-migration-guide.md @@ -16,7 +16,7 @@ In Svelte 4, a `let` declaration at the top level of a component was implicitly ```svelte ``` @@ -31,8 +31,8 @@ In Svelte 4, a `$:` statement at the top level of a component could be used to d ```svelte ``` @@ -42,7 +42,8 @@ A `$:` statement could also be used to create side effects. In Svelte 5, this is ```svelte ``` @@ -105,8 +106,8 @@ In Svelte 5, the `$props` rune makes this straightforward without any additional ```svelte @@ -192,9 +193,9 @@ This function is deprecated in Svelte 5. Instead, components should accept _call ```svelte From 74917ae7039e512d6bf26b2b04f433eea7da8cd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:25:35 -0400 Subject: [PATCH 055/145] Version Packages (#15501) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/brown-rockets-shake.md | 5 ----- .changeset/cuddly-chefs-refuse.md | 5 ----- .changeset/curvy-countries-flow.md | 5 ----- .changeset/gold-hairs-jog.md | 5 ----- .changeset/hungry-dancers-tap.md | 5 ----- .changeset/plenty-bats-lay.md | 5 ----- .changeset/two-spies-lie.md | 5 ----- packages/svelte/CHANGELOG.md | 18 ++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 10 files changed, 20 insertions(+), 37 deletions(-) delete mode 100644 .changeset/brown-rockets-shake.md delete mode 100644 .changeset/cuddly-chefs-refuse.md delete mode 100644 .changeset/curvy-countries-flow.md delete mode 100644 .changeset/gold-hairs-jog.md delete mode 100644 .changeset/hungry-dancers-tap.md delete mode 100644 .changeset/plenty-bats-lay.md delete mode 100644 .changeset/two-spies-lie.md diff --git a/.changeset/brown-rockets-shake.md b/.changeset/brown-rockets-shake.md deleted file mode 100644 index 3772a88f6e..0000000000 --- a/.changeset/brown-rockets-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: invalidate parent effects when child effects update parent dependencies diff --git a/.changeset/cuddly-chefs-refuse.md b/.changeset/cuddly-chefs-refuse.md deleted file mode 100644 index 6672ac4ab3..0000000000 --- a/.changeset/cuddly-chefs-refuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly match `:has()` selector during css pruning diff --git a/.changeset/curvy-countries-flow.md b/.changeset/curvy-countries-flow.md deleted file mode 100644 index 6ef8545804..0000000000 --- a/.changeset/curvy-countries-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: replace `undefined` with `void 0` to avoid edge case diff --git a/.changeset/gold-hairs-jog.md b/.changeset/gold-hairs-jog.md deleted file mode 100644 index eaafced314..0000000000 --- a/.changeset/gold-hairs-jog.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow global-like pseudo-selectors refinement diff --git a/.changeset/hungry-dancers-tap.md b/.changeset/hungry-dancers-tap.md deleted file mode 100644 index 51b2f86019..0000000000 --- a/.changeset/hungry-dancers-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: don't distribute unused types definitions diff --git a/.changeset/plenty-bats-lay.md b/.changeset/plenty-bats-lay.md deleted file mode 100644 index cd5ce66e42..0000000000 --- a/.changeset/plenty-bats-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts diff --git a/.changeset/two-spies-lie.md b/.changeset/two-spies-lie.md deleted file mode 100644 index 2ea7fd6136..0000000000 --- a/.changeset/two-spies-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 65b3edd1fd..e10a606fe4 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,23 @@ # svelte +## 5.23.1 + +### Patch Changes + +- fix: invalidate parent effects when child effects update parent dependencies ([#15506](https://github.com/sveltejs/svelte/pull/15506)) + +- fix: correctly match `:has()` selector during css pruning ([#15277](https://github.com/sveltejs/svelte/pull/15277)) + +- fix: replace `undefined` with `void 0` to avoid edge case ([#15511](https://github.com/sveltejs/svelte/pull/15511)) + +- fix: allow global-like pseudo-selectors refinement ([#15313](https://github.com/sveltejs/svelte/pull/15313)) + +- chore: don't distribute unused types definitions ([#15473](https://github.com/sveltejs/svelte/pull/15473)) + +- fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts ([#15492](https://github.com/sveltejs/svelte/pull/15492)) + +- fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg ([#15516](https://github.com/sveltejs/svelte/pull/15516)) + ## 5.23.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c74c9d34ca..6f10b2a9ea 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.23.0", + "version": "5.23.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 5f06fd0753..32a50f3bce 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.23.0'; +export const VERSION = '5.23.1'; export const PUBLIC_VERSION = '5'; From 5b9f0df8ee97ba43a3d3af18f99f2dd44bd86965 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 18 Mar 2025 11:51:26 +0100 Subject: [PATCH 056/145] fix: don't hoist listeners that access non hoistable snippets (#15534) * fix: don't hoist listeners that access non hoistable snippets * chore: add comment * chore: fix auto import fumble --- .changeset/thick-pans-fold.md | 5 +++++ .../compiler/phases/2-analyze/visitors/Attribute.js | 9 +++++++++ .../unhoist-function-accessing-snippet/_config.js | 12 ++++++++++++ .../unhoist-function-accessing-snippet/main.svelte | 12 ++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 .changeset/thick-pans-fold.md create mode 100644 packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte diff --git a/.changeset/thick-pans-fold.md b/.changeset/thick-pans-fold.md new file mode 100644 index 0000000000..b5b5cee53e --- /dev/null +++ b/.changeset/thick-pans-fold.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't hoist listeners that access non hoistable snippets diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 9124a8822f..3ba81767cc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -183,6 +183,15 @@ function get_delegated_event(event_name, handler, context) { const binding = scope.get(reference); const local_binding = context.state.scope.get(reference); + // if the function access a snippet that can't be hoisted we bail out + if ( + local_binding !== null && + local_binding.initial?.type === 'SnippetBlock' && + !local_binding.initial.metadata.can_hoist + ) { + return unhoisted; + } + // If we are referencing a binding that is shadowed in another scope then bail out. if (local_binding !== null && binding !== null && local_binding.node !== binding.node) { return unhoisted; diff --git a/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/_config.js b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/_config.js new file mode 100644 index 0000000000..b1229f5a8a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, errors }) { + const button = target.querySelector('button'); + flushSync(() => { + button?.click(); + }); + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte new file mode 100644 index 0000000000..e909d77fd6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte @@ -0,0 +1,12 @@ + + + + +{#snippet snip()} + snippet {x} +{/snippet} \ No newline at end of file From 0af6f20c77c209a5ea5691f2d1c15e0e359fbed6 Mon Sep 17 00:00:00 2001 From: henrykrinkle01 <162001892+henrykrinkle01@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:54:32 +0700 Subject: [PATCH 057/145] Fix grammar (#15533) --- documentation/docs/06-runtime/02-context.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 30799215b6..87b93a92b2 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -30,7 +30,7 @@ export const myGlobalState = $state({ This has a few drawbacks though: - it only safely works when your global state is only used client-side - for example, when you're building a single page application that does not render any of your components on the server. If your state ends up being managed and updated on the server, it could end up being shared between sessions and/or users, causing bugs -- it may give the false impression that certain state is global when in reality it should only used in a certain part of your app +- it may give the false impression that certain state is global when in reality it should only be used in a certain part of your app To solve these drawbacks, Svelte provides a few `context` primitives which alleviate these problems. From 190c0c7653435fd983d13a9594f5d70bdb4dd26f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:28:29 -0400 Subject: [PATCH 058/145] Version Packages (#15536) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/thick-pans-fold.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/thick-pans-fold.md diff --git a/.changeset/thick-pans-fold.md b/.changeset/thick-pans-fold.md deleted file mode 100644 index b5b5cee53e..0000000000 --- a/.changeset/thick-pans-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't hoist listeners that access non hoistable snippets diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index e10a606fe4..6461df1d25 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.23.2 + +### Patch Changes + +- fix: don't hoist listeners that access non hoistable snippets ([#15534](https://github.com/sveltejs/svelte/pull/15534)) + ## 5.23.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6f10b2a9ea..d005eca0b9 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.23.1", + "version": "5.23.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 32a50f3bce..191b52ecef 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.23.1'; +export const VERSION = '5.23.2'; export const PUBLIC_VERSION = '5'; From 8f940ee0ff12be2ae6b393b4e021507d3f3e2068 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Mar 2025 10:41:13 -0400 Subject: [PATCH 059/145] docs: use function bindings in "when not to use effect" (#15544) --- documentation/docs/02-runes/04-$effect.md | 42 ++++------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index e346bceba8..6a2b565aea 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -280,7 +280,7 @@ You might be tempted to do something convoluted with effects to link one value t ``` -Instead, use callbacks where possible ([demo](/playground/untitled#H4sIAAAAAAAACo1SMW6EMBD8imWluFMSIEUaDiKlvy5lSOHjlhOSMRZeTiDkv8deMEEJRcqdmZ1ZjzzxqpZgePo5cRw18JQA_sSVaPz0rnVk7iDRYxdhYA8vW4Wg0NnwzJRdrfGtUAVKQIYtCsly9pIkp4AZ7cQOezAoEA7JcWUkVBuCdol0dNWrEutWsV5fHfnhPQ5wZJMnCwyejxCh6G6A0V3IHk4zu_jOxzzPBxBld83PTr7xXrb3rUNw8PbiYJ3FP22oTIoLSComq5XuXTeu8LzgnVA3KDgj13wiQ8taRaJ82rzXskYM-URRlsXktejjgNLoo9e4fyf70_8EnwncySX1GuunX6kGRwnzR_BgaPNaGy3FmLJKwrCUeBM6ZUn0Cs2mOlp3vwthQJ5i14P9st9vZqQlsQIAAA==)): +Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE51SsW6DMBT8FcvqABINdOhCIFKXTt06lg4GHpElYyz8iECIf69tcIIipo6-u3f3fPZMJWuBpvRzkBXyTpKSy5rLq6YRbbgATdOfmeKkrMgCBt9GPpQ66RsItFjJNBzhVScRJBobmumq5wovhSxQABLskAmSk7ckOXtMKyM22ItGhhAk4Z0R0OwIN-tIQzd-90HVhvy2HsGNiQFCMltBgd7XoecV2xzXNV7XaEcth7ZfRv7kujnsTX2Qd7USb5rFjwZkJlgJwpWRcakG04cpOS9oz-QVCuoeInXW-RyEJL-sG0b7Wy6kZWM-u7CFxM5tdrIl9qg72vB74H-y7T2iXROHyVb0CLanp1yNk4D1A1jQ91hzrQSbUtIIGLcir0ylJDm9Q7urz42bX4UwIk2xH2D5Xf4A7SeMcMQCAAA=)): ```svelte ``` -If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/playground/untitled#H4sIAAAAAAAACpWRwW6DMBBEf8WyekikFOihFwcq9TvqHkyyQUjGsfCCQMj_XnvBNKpy6Qn2DTOD1wu_tRocF18Lx9kCFwT4iRvVxenT2syNoDGyWjl4xi93g2AwxPDSXfrW4oc0EjUgwzsqzSr2VhTnxJwNHwf24lAhHIpjVDZNwy1KS5wlNoGMSg9wOCYksQccerMlv65p51X0p_Xpdt_4YEy9yTkmV3z4MJT579-bUqsaNB2kbI0dwlnCgirJe2UakJzVrbkKaqkWivasU1O1ULxnOVk3JU-Uxti0p_-vKO4no_enbQ_yXhnZn0aHs4b1jiJMK7q2zmo1C3bTMG3LaZQVrMjeoSPgaUtkDxePMCEX2Ie6b_8D4WyJJEwCAAA=)): - -```svelte - - - - - -``` - If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](svelte#untrack). From 701f085c82d11e6064433731d36b33d4894c706a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Mar 2025 10:43:45 -0400 Subject: [PATCH 060/145] docs: rewrite context docs (#15541) --- documentation/docs/06-runtime/02-context.md | 164 ++++++++++---------- 1 file changed, 86 insertions(+), 78 deletions(-) diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 87b93a92b2..b698323a04 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -2,129 +2,137 @@ title: Context --- - +Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling'). The parent component sets context with `setContext(key, value)`... -Most state is component-level state that lives as long as its component lives. There's also section-wide or app-wide state however, which also needs to be handled somehow. - -The easiest way to do that is to create global state and just import that. +```svelte + + ``` +...and the child retrieves it with `getContext`: + ```svelte - + + +

{message}, inside Child.svelte

``` -This has a few drawbacks though: +This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) ([demo](/playground/untitled#H4sIAAAAAAAAE42Q3W6DMAyFX8WyJgESK-oto6hTX2D3YxcM3IIUQpR40yqUd58CrCXsp7tL7HNsf2dAWXaEKR56yfTBGOOxFWQwfR6Qz8q1XAHjL-GjUhvzToJd7bU09FO9ctMkG0wxM5VuFeeFLLjtVK8ZnkpNkuGo-w6CTTJ9Z3PwsBAemlbUF934W8iy5DpaZtOUcU02-ZLcaS51jHEkTFm_kY1_wfOO8QnXrb8hBzDEc6pgZ4gFoyz4KgiD7nxfTe8ghqAhIfrJ46cTzVZBbkPlODVJsLCDO6V7ZcJoncyw1yRr0hd1GNn_ZbEM3I9i1bmVxOlWElUvDUNHxpQngt3C4CXzjS1rtvkw22wMrTRtTbC8Lkuabe7jvthPPe3DofYCAAA=)): + +```svelte + + + +``` -- it only safely works when your global state is only used client-side - for example, when you're building a single page application that does not render any of your components on the server. If your state ends up being managed and updated on the server, it could end up being shared between sessions and/or users, causing bugs -- it may give the false impression that certain state is global when in reality it should only be used in a certain part of your app +The key (`'my-context'`, in the example above) and the context itself can be any JavaScript value. -To solve these drawbacks, Svelte provides a few `context` primitives which alleviate these problems. +In addition to [`setContext`](svelte#setContext) and [`getContext`](svelte#getContext), Svelte exposes [`hasContext`](svelte#hasContext) and [`getAllContexts`](svelte#getAllContexts) functions. -## Setting and getting context +## Using context with state -To associate an arbitrary object with the current component, use `setContext`. +You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAAAE41R0W6DMAz8FSuaBNUQdK8MkKZ-wh7HHihzu6hgosRMm1D-fUpSVNq12x4iEvvOx_kmQU2PIhfP3DCCJGgHYvxkkYid7NCI_GUS_KUcxhVEMjOelErNB3bsatvG4LW6n0ZsRC4K02qpuKqpZtmrQTNMYJA3QRAs7PTQQxS40eMCt3mX3duxnWb-lS5h7nTI0A4jMWoo4c44P_Hku-zrOazdy64chWo-ScfRkRgl8wgHKrLTH1OxHZkHgoHaTraHcopXUFYzPPVfuC_hwQaD1GrskdiNCdQwJljJqlvXfyqVsA5CGg0uRUQifHw56xFtciO75QrP07vo_JXf_tf8yK2ezDKY_ZWt_1y2qqYzv7bI1IW1V_sN19m-07wCAAA=))... ```svelte + + + + + + ``` -The context is then available to children of the component (including slotted content) with `getContext`. +...though note that if you _reassign_ `counter` instead of updating it, you will 'break the link' — in other words instead of this... ```svelte - + ``` -`setContext` and `getContext` solve the above problems: +...you must do this: -- the state is not global, it's scoped to the component. That way it's safe to render your components on the server and not leak state -- it's clear that the state is not global but rather scoped to a specific component tree and therefore can't be used in other parts of your app +```svelte + +``` -> [!NOTE] `setContext`/`getContext` must be called during component initialisation. +Svelte will warn you if you get it wrong. -Context is not inherently reactive. If you need reactive values in context then you can pass a `$state` object into context, whose properties _will_ be reactive. +## Type-safe context -```svelte - - +```js +/// file: context.js +// @filename: ambient.d.ts +interface User {} - -``` +// @filename: index.js +// ---cut--- +import { getContext, setContext } from 'svelte'; -```svelte - - +/** @param {User} user */ +export function setUserContext(user) { + setContext(key, user); +} -

Count is {value.count}

+export function getUserContext() { + return /** @type {User} */ (getContext(key)); +} ``` -To check whether a given `key` has been set in the context of a parent component, use `hasContext`. +## Replacing global state -```svelte - + // ... +}); ``` -You can also retrieve the whole context map that belongs to the closest parent component using `getAllContexts`. This is useful, for example, if you programmatically create a component and want to pass the existing context to it. +In many cases this is perfectly fine, but there is a risk: if you mutate the state during server-side rendering (which is discouraged, but entirely possible!)... ```svelte + ``` -## Encapsulating context interactions - -The above methods are very unopinionated about how to use them. When your app grows in scale, it's worthwhile to encapsulate setting and getting the context into functions and properly type them. - -```ts -// @errors: 2304 -import { getContext, setContext } from 'svelte'; - -let userKey = Symbol('user'); - -export function setUserContext(user: User) { - setContext(userKey, user); -} - -export function getUserContext(): User { - return getContext(userKey) as User; -} -``` +...then the data may be accessible by the _next_ user. Context solves this problem because it is not shared between requests. From 99ca7a4d7f5948f94c2fa0137a481b57c4a6c17b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Mar 2025 14:06:20 -0400 Subject: [PATCH 061/145] chore: create stack lazily when proxying value (#15547) --- packages/svelte/src/internal/client/proxy.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 4c262880f1..29828a7c99 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -25,11 +25,6 @@ import { tracing_mode_flag } from '../flags/index.js'; * @returns {T} */ export function proxy(value, parent = null, prev) { - /** @type {Error | null} */ - var stack = null; - if (DEV && tracing_mode_flag) { - stack = get_stack('CreatedAt'); - } // if non-proxyable, or is already a proxy, return `value` if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { return value; @@ -46,6 +41,8 @@ export function proxy(value, parent = null, prev) { var is_proxied_array = is_array(value); var version = source(0); + var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; + if (is_proxied_array) { // We need to create the length source eagerly to ensure that // mutations to the array are properly synced with our proxy From c436b6cdbe01577b219ddbfd09e23c5765515004 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Mar 2025 17:21:26 -0400 Subject: [PATCH 062/145] fix: simplify set calls for proxyable values (#15548) * chore: simplify set calls for proxyable values * changeset --- .changeset/nine-laws-rush.md | 5 ++ .../phases/3-transform/client/types.d.ts | 2 +- .../phases/3-transform/client/utils.js | 8 ---- .../client/visitors/AssignmentExpression.js | 46 +++++++------------ .../3-transform/client/visitors/ClassBody.js | 15 ++---- .../client/visitors/shared/declarations.js | 4 +- .../src/internal/client/reactivity/sources.js | 8 +++- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 4 +- .../_expected/client/index.svelte.js | 4 +- .../_expected/client/index.svelte.js | 2 +- 11 files changed, 40 insertions(+), 60 deletions(-) create mode 100644 .changeset/nine-laws-rush.md diff --git a/.changeset/nine-laws-rush.md b/.changeset/nine-laws-rush.md new file mode 100644 index 0000000000..e0a0fc15a0 --- /dev/null +++ b/.changeset/nine-laws-rush.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: simplify set calls for proxyable values diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 63fe3223cf..243e1c64a3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -30,7 +30,7 @@ export interface ClientTransformState extends TransformState { /** turn `foo` into e.g. `$.get(foo)` */ read: (id: Identifier) => Expression; /** turn `foo = bar` into e.g. `$.set(foo, bar)` */ - assign?: (node: Identifier, value: Expression) => Expression; + assign?: (node: Identifier, value: Expression, proxy?: boolean) => Expression; /** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */ mutate?: (node: Identifier, mutation: AssignmentExpression | UpdateExpression) => Expression; /** turn `foo++` into e.g. `$.update(foo)` */ 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 421118cf68..28e3fabb19 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -45,14 +45,6 @@ export function build_getter(node, state) { return node; } -/** - * @param {Expression} value - * @param {Expression} previous - */ -export function build_proxy_reassignment(value, previous) { - return dev ? b.call('$.proxy', value, b.null, previous) : b.call('$.proxy', value); -} - /** * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node * @param {ComponentContext} context diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index a8c615af93..150c56e166 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,5 +1,4 @@ -/** @import { Location } from 'locate-character' */ -/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Literal, MemberExpression, Pattern } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types.js' */ import * as b from '../../../../utils/builders.js'; @@ -8,8 +7,8 @@ import { get_attribute_expression, is_event_attribute } from '../../../../utils/ast.js'; -import { dev, filename, is_ignored, locate_node, locator } from '../../../../state.js'; -import { build_proxy_reassignment, should_proxy } from '../utils.js'; +import { dev, is_ignored, locate_node } from '../../../../state.js'; +import { should_proxy } from '../utils.js'; import { visit_assignment_expression } from '../../shared/assignments.js'; /** @@ -65,21 +64,12 @@ function build_assignment(operator, left, right, context) { context.visit(build_assignment_value(operator, left, right)) ); - if ( + const needs_proxy = private_state.kind === 'state' && is_non_coercive_operator(operator) && - should_proxy(value, context.state.scope) - ) { - value = build_proxy_reassignment(value, b.member(b.this, private_state.id)); - } - - if (context.state.in_constructor) { - // inside the constructor, we can assign to `this.#foo.v` rather than using `$.set`, - // since nothing is tracking the signal at this point - return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value); - } + should_proxy(value, context.state.scope); - return b.call('$.set', left, value); + return b.call('$.set', left, value, needs_proxy && b.true); } } @@ -113,20 +103,18 @@ function build_assignment(operator, left, right, context) { context.visit(build_assignment_value(operator, left, right)) ); - if ( + return transform.assign( + object, + value, !is_primitive && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'raw_state' && - binding.kind !== 'store_sub' && - context.state.analysis.runes && - should_proxy(right, context.state.scope) && - is_non_coercive_operator(operator) - ) { - value = build_proxy_reassignment(value, object); - } - - return transform.assign(object, value); + binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && + binding.kind !== 'raw_state' && + binding.kind !== 'store_sub' && + context.state.analysis.runes && + should_proxy(right, context.state.scope) && + is_non_coercive_operator(operator) + ); } // mutation diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index ed800e5226..5787b590a8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -5,7 +5,7 @@ import { dev, is_ignored } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; import { regex_invalid_identifier_chars } from '../../../patterns.js'; import { get_rune } from '../../../scope.js'; -import { build_proxy_reassignment, should_proxy } from '../utils.js'; +import { should_proxy } from '../utils.js'; /** * @param {ClassBody} node @@ -142,29 +142,20 @@ export function ClassBody(node, context) { // get foo() { return this.#foo; } body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))])); - if (field.kind === 'state') { + if (field.kind === 'state' || field.kind === 'raw_state') { // set foo(value) { this.#foo = value; } const value = b.id('value'); - const prev = b.member(b.this, field.id); body.push( b.method( 'set', definition.key, [value], - [b.stmt(b.call('$.set', member, build_proxy_reassignment(value, prev)))] + [b.stmt(b.call('$.set', member, value, field.kind === 'state' && b.true))] ) ); } - if (field.kind === 'raw_state') { - // set foo(value) { this.#foo = value; } - const value = b.id('value'); - body.push( - b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))]) - ); - } - if (dev && (field.kind === 'derived' || field.kind === 'derived_by')) { body.push( b.method( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index 0bd8c352f6..a13ecfed2c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -24,8 +24,8 @@ export function add_state_transformers(context) { ) { context.state.transform[name] = { read: binding.declaration_kind === 'var' ? (node) => b.call('$.safe_get', node) : get_value, - assign: (node, value) => { - let call = b.call('$.set', node, value); + assign: (node, value, proxy = false) => { + let call = b.call('$.set', node, value, proxy && b.true); if (context.state.scope.get(`$${node.name}`)?.kind === 'store_sub') { call = b.call('$.store_unsub', call, b.literal(`$${node.name}`), b.id('$$stores')); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 49584e8626..92508945c9 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -33,6 +33,7 @@ import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; +import { proxy } from '../proxy.js'; export let inspect_effects = new Set(); export const old_values = new Map(); @@ -143,9 +144,10 @@ export function mutate(source, value) { * @template V * @param {Source} source * @param {V} value + * @param {boolean} [should_proxy] * @returns {V} */ -export function set(source, value) { +export function set(source, value, should_proxy = false) { if ( active_reaction !== null && !untracking && @@ -158,7 +160,9 @@ export function set(source, value) { e.state_unsafe_mutation(); } - return internal_set(source, value); + let new_value = should_proxy ? proxy(value, null, source) : value; + + return internal_set(source, new_value); } /** diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index fa990b33ee..390e86a351 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -23,7 +23,7 @@ export default function Bind_component_snippet($$anchor) { return $.get(value); }, set value($$value) { - $.set(value, $.proxy($$value)); + $.set(value, $$value, true); } }); diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js index 2898f31a6f..2133974176 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js @@ -12,14 +12,14 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro } set a(value) { - $.set(this.#a, $.proxy(value)); + $.set(this.#a, value, true); } #b = $.state(); constructor() { this.a = 1; - this.#b.v = 2; + $.set(this.#b, 2); } } diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js index 9651713c52..47f297bce9 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js @@ -8,8 +8,8 @@ let d = 4; export function update(array) { ( - $.set(a, $.proxy(array[0])), - $.set(b, $.proxy(array[1])) + $.set(a, array[0], true), + $.set(b, array[1], true) ); [c, d] = array; diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index c545608bca..762a23754c 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -13,7 +13,7 @@ export default function Function_prop_no_getter($$anchor) { Button($$anchor, { onmousedown: () => $.set(count, $.get(count) + 1), onmouseup, - onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))), + onmouseenter: () => $.set(count, plusOne($.get(count)), true), children: ($$anchor, $$slotProps) => { $.next(); From c7ce9fc004325d5e5c957943f4c5e342e8304905 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 20 Mar 2025 14:19:42 -0400 Subject: [PATCH 063/145] fix benchmarks (#15560) --- benchmarking/compare/index.js | 1 - benchmarking/compare/runner.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js index a5fc6d10a9..9d8d279c35 100644 --- a/benchmarking/compare/index.js +++ b/benchmarking/compare/index.js @@ -2,7 +2,6 @@ import fs from 'node:fs'; import path from 'node:path'; import { execSync, fork } from 'node:child_process'; import { fileURLToPath } from 'node:url'; -import { benchmarks } from '../benchmarks.js'; // if (execSync('git status --porcelain').toString().trim()) { // console.error('Working directory is not clean'); diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js index 6fa58e2bac..a2e8646379 100644 --- a/benchmarking/compare/runner.js +++ b/benchmarking/compare/runner.js @@ -1,7 +1,7 @@ -import { benchmarks } from '../benchmarks.js'; +import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js'; const results = []; -for (const benchmark of benchmarks) { +for (const benchmark of reactivity_benchmarks) { const result = await benchmark(); console.error(result.benchmark); results.push(result); From 6915c12b583f4d62c125161be07f5d09573918c9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 20 Mar 2025 16:04:00 -0400 Subject: [PATCH 064/145] feat: allow state created in deriveds/effects to be written/read locally without self-invalidation (#15553) * move parent property onto Signal * don't self-invalidate when updating a source create inside current reaction * lazily create deep state with parent reaction * no need to push_derived_source with mutable_state, as it never coexists with $.derived * reduce indirection * remove state_unsafe_local_read error * changeset * tests * fix test * inelegant fix * remove arg * tweak * some progress * more * tidy up * parent -> p * tmp * alternative approach * tidy up * reduce diff size * more * update comment --- .changeset/dirty-pianos-sparkle.md | 5 ++ .../98-reference/.generated/client-errors.md | 6 -- .../svelte/messages/client-errors/errors.md | 4 -- .../3-transform/client/transform-client.js | 5 +- .../client/visitors/VariableDeclaration.js | 4 +- .../svelte/src/internal/client/constants.js | 1 + packages/svelte/src/internal/client/errors.js | 15 ----- packages/svelte/src/internal/client/index.js | 9 ++- packages/svelte/src/internal/client/proxy.js | 67 +++++++++++++++---- .../src/internal/client/reactivity/sources.js | 57 +++++----------- .../svelte/src/internal/client/runtime.js | 30 +++++---- packages/svelte/tests/signals/test.ts | 49 ++++++++++++-- 12 files changed, 151 insertions(+), 101 deletions(-) create mode 100644 .changeset/dirty-pianos-sparkle.md diff --git a/.changeset/dirty-pianos-sparkle.md b/.changeset/dirty-pianos-sparkle.md new file mode 100644 index 0000000000..b3e4dd1d8c --- /dev/null +++ b/.changeset/dirty-pianos-sparkle.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow state created in deriveds/effects to be written/read locally without self-invalidation diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 0beb3cb9a9..62d9c3302a 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -122,12 +122,6 @@ Property descriptors defined on `$state` objects must contain `value` and always Cannot set prototype of `$state` object ``` -### state_unsafe_local_read - -``` -Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state -``` - ### state_unsafe_mutation ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index ab4d1519c1..bc8ec36256 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -80,10 +80,6 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > Cannot set prototype of `$state` object -## state_unsafe_local_read - -> Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state - ## state_unsafe_mutation > Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index ac8263b916..0bdfbae746 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -219,7 +219,10 @@ export function client_component(analysis, options) { for (const [name, binding] of analysis.instance.scope.declarations) { if (binding.kind === 'legacy_reactive') { legacy_reactive_declarations.push( - b.const(name, b.call('$.mutable_state', undefined, analysis.immutable ? b.true : undefined)) + b.const( + name, + b.call('$.mutable_source', undefined, analysis.immutable ? b.true : undefined) + ) ); } if (binding.kind === 'store_sub') { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index baffc5dec3..3a914fb560 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -299,7 +299,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) { return [ b.declarator( declarator.id, - b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined) + b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) ) ]; } @@ -314,7 +314,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) { return b.declarator( path.node, binding?.kind === 'state' - ? b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined) + ? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) : value ); }) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index a4840ce4eb..21377c1cc8 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -20,6 +20,7 @@ export const LEGACY_DERIVED_PROP = 1 << 17; export const INSPECT_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 19; export const EFFECT_HAS_DERIVED = 1 << 20; +export const EFFECT_IS_UPDATING = 1 << 21; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 682816e1d6..8a5b5033a7 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -307,21 +307,6 @@ export function state_prototype_fixed() { } } -/** - * Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state - * @returns {never} - */ -export function state_unsafe_local_read() { - if (DEV) { - const error = new Error(`state_unsafe_local_read\nReading state that was created inside the same derived is forbidden. Consider using \`untrack\` to read locally created state\nhttps://svelte.dev/e/state_unsafe_local_read`); - - error.name = 'Svelte error'; - throw error; - } else { - throw new Error(`https://svelte.dev/e/state_unsafe_local_read`); - } -} - /** * Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` * @returns {never} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 31da00dbb4..723ff57678 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -113,7 +113,14 @@ export { user_effect, user_pre_effect } from './reactivity/effects.js'; -export { mutable_state, mutate, set, state, update, update_pre } from './reactivity/sources.js'; +export { + mutable_source, + mutate, + set, + source as state, + update, + update_pre +} from './reactivity/sources.js'; export { prop, rest_props, diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 29828a7c99..9c3c0cf29f 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,6 +1,6 @@ /** @import { ProxyMetadata, Source } from '#client' */ import { DEV } from 'esm-env'; -import { get, active_effect } from './runtime.js'; +import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js'; import { component_context } from './context.js'; import { array_prototype, @@ -17,14 +17,16 @@ import * as e from './errors.js'; import { get_stack } from './dev/tracing.js'; import { tracing_mode_flag } from '../flags/index.js'; +/** @type {ProxyMetadata | null} */ +var parent_metadata = null; + /** * @template T * @param {T} value - * @param {ProxyMetadata | null} [parent] * @param {Source} [prev] dev mode only * @returns {T} */ -export function proxy(value, parent = null, prev) { +export function proxy(value, prev) { // if non-proxyable, or is already a proxy, return `value` if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { return value; @@ -42,6 +44,31 @@ export function proxy(value, parent = null, prev) { var version = source(0); var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; + var reaction = active_reaction; + + /** + * @template T + * @param {() => T} fn + */ + var with_parent = (fn) => { + var previous_reaction = active_reaction; + set_active_reaction(reaction); + + /** @type {T} */ + var result; + + if (DEV) { + var previous_metadata = parent_metadata; + parent_metadata = metadata; + result = fn(); + parent_metadata = previous_metadata; + } else { + result = fn(); + } + + set_active_reaction(previous_reaction); + return result; + }; if (is_proxied_array) { // We need to create the length source eagerly to ensure that @@ -54,7 +81,7 @@ export function proxy(value, parent = null, prev) { if (DEV) { metadata = { - parent, + parent: parent_metadata, owners: null }; @@ -66,7 +93,7 @@ export function proxy(value, parent = null, prev) { metadata.owners = prev_owners ? new Set(prev_owners) : null; } else { metadata.owners = - parent === null + parent_metadata === null ? component_context !== null ? new Set([component_context.function]) : null @@ -92,10 +119,13 @@ export function proxy(value, parent = null, prev) { var s = sources.get(prop); if (s === undefined) { - s = source(descriptor.value, stack); + s = with_parent(() => source(descriptor.value, stack)); sources.set(prop, s); } else { - set(s, proxy(descriptor.value, metadata)); + set( + s, + with_parent(() => proxy(descriptor.value)) + ); } return true; @@ -106,7 +136,10 @@ export function proxy(value, parent = null, prev) { if (s === undefined) { if (prop in target) { - sources.set(prop, source(UNINITIALIZED, stack)); + sources.set( + prop, + with_parent(() => source(UNINITIALIZED, stack)) + ); } } else { // When working with arrays, we need to also ensure we update the length when removing @@ -140,7 +173,7 @@ export function proxy(value, parent = null, prev) { // create a source, but only if it's an own property and not a prototype property if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { - s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata), stack); + s = with_parent(() => source(proxy(exists ? target[prop] : UNINITIALIZED), stack)); sources.set(prop, s); } @@ -208,7 +241,7 @@ export function proxy(value, parent = null, prev) { (active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) ) { if (s === undefined) { - s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, stack); + s = with_parent(() => source(has ? proxy(target[prop]) : UNINITIALIZED, stack)); sources.set(prop, s); } @@ -235,7 +268,7 @@ export function proxy(value, parent = null, prev) { // If the item exists in the original, we need to create a uninitialized source, // else a later read of the property would result in a source being created with // the value of the original item at that index. - other_s = source(UNINITIALIZED, stack); + other_s = with_parent(() => source(UNINITIALIZED, stack)); sources.set(i + '', other_s); } } @@ -247,13 +280,19 @@ export function proxy(value, parent = null, prev) { // object property before writing to that property. if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { - s = source(undefined, stack); - set(s, proxy(value, metadata)); + s = with_parent(() => source(undefined, stack)); + set( + s, + with_parent(() => proxy(value)) + ); sources.set(prop, s); } } else { has = s.v !== UNINITIALIZED; - set(s, proxy(value, metadata)); + set( + s, + with_parent(() => proxy(value)) + ); } if (DEV) { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 92508945c9..cac8431b4e 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -11,8 +11,8 @@ import { untrack, increment_write_version, update_effect, - derived_sources, - set_derived_sources, + reaction_sources, + set_reaction_sources, check_dirtiness, untracking, is_destroying_effect @@ -27,7 +27,8 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT + ROOT_EFFECT, + EFFECT_IS_UPDATING } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -51,6 +52,7 @@ export function set_inspect_effects(v) { * @param {Error | null} [stack] * @returns {Source} */ +// TODO rename this to `state` throughout the codebase export function source(v, stack) { /** @type {Value} */ var signal = { @@ -62,6 +64,14 @@ export function source(v, stack) { wv: 0 }; + if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { + if (reaction_sources === null) { + set_reaction_sources([signal]); + } else { + reaction_sources.push(signal); + } + } + if (DEV && tracing_mode_flag) { signal.created = stack ?? get_stack('CreatedAt'); signal.debug = null; @@ -70,14 +80,6 @@ export function source(v, stack) { return signal; } -/** - * @template V - * @param {V} v - */ -export function state(v) { - return push_derived_source(source(v)); -} - /** * @template V * @param {V} initial_value @@ -100,33 +102,6 @@ export function mutable_source(initial_value, immutable = false) { return s; } -/** - * @template V - * @param {V} v - * @param {boolean} [immutable] - * @returns {Source} - */ -export function mutable_state(v, immutable = false) { - return push_derived_source(mutable_source(v, immutable)); -} - -/** - * @template V - * @param {Source} source - */ -/*#__NO_SIDE_EFFECTS__*/ -function push_derived_source(source) { - if (active_reaction !== null && !untracking && (active_reaction.f & DERIVED) !== 0) { - if (derived_sources === null) { - set_derived_sources([source]); - } else { - derived_sources.push(source); - } - } - - return source; -} - /** * @template V * @param {Value} source @@ -153,14 +128,12 @@ export function set(source, value, should_proxy = false) { !untracking && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 && - // If the source was created locally within the current derived, then - // we allow the mutation. - (derived_sources === null || !derived_sources.includes(source)) + !reaction_sources?.includes(source) ) { e.state_unsafe_mutation(); } - let new_value = should_proxy ? proxy(value, null, source) : value; + let new_value = should_proxy ? proxy(value, source) : value; return internal_set(source, new_value); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 0a65c6e45a..74b58ee1a9 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -22,7 +22,8 @@ import { ROOT_EFFECT, LEGACY_DERIVED_PROP, DISCONNECTED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + EFFECT_IS_UPDATING } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; @@ -87,17 +88,17 @@ export function set_active_effect(effect) { } /** - * When sources are created within a derived, we record them so that we can safely allow - * local mutations to these sources without the side-effect error being invoked unnecessarily. + * When sources are created within a reaction, reading and writing + * them should not cause a re-run * @type {null | Source[]} */ -export let derived_sources = null; +export let reaction_sources = null; /** * @param {Source[] | null} sources */ -export function set_derived_sources(sources) { - derived_sources = sources; +export function set_reaction_sources(sources) { + reaction_sources = sources; } /** @@ -367,6 +368,9 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) for (var i = 0; i < reactions.length; i++) { var reaction = reactions[i]; + + if (reaction_sources?.includes(signal)) continue; + if ((reaction.f & DERIVED) !== 0) { schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); } else if (effect === reaction) { @@ -391,9 +395,10 @@ export function update_reaction(reaction) { var previous_untracked_writes = untracked_writes; var previous_reaction = active_reaction; var previous_skip_reaction = skip_reaction; - var prev_derived_sources = derived_sources; + var previous_reaction_sources = reaction_sources; var previous_component_context = component_context; var previous_untracking = untracking; + var flags = reaction.f; new_deps = /** @type {null | Value[]} */ (null); @@ -403,11 +408,13 @@ export function update_reaction(reaction) { (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; - derived_sources = null; + reaction_sources = null; set_component_context(reaction.ctx); untracking = false; read_version++; + reaction.f |= EFFECT_IS_UPDATING; + try { var result = /** @type {Function} */ (0, reaction.fn)(); var deps = reaction.deps; @@ -477,9 +484,11 @@ export function update_reaction(reaction) { untracked_writes = previous_untracked_writes; active_reaction = previous_reaction; skip_reaction = previous_skip_reaction; - derived_sources = prev_derived_sources; + reaction_sources = previous_reaction_sources; set_component_context(previous_component_context); untracking = previous_untracking; + + reaction.f ^= EFFECT_IS_UPDATING; } } @@ -866,9 +875,6 @@ export function get(signal) { // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { - if (derived_sources !== null && derived_sources.includes(signal)) { - e.state_unsafe_local_read(); - } var deps = active_reaction.deps; if (signal.rv < read_version) { signal.rv = read_version; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index ef4cf16d3b..72f99c90e5 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -8,7 +8,12 @@ import { render_effect, user_effect } from '../../src/internal/client/reactivity/effects'; -import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; +import { + source as state, + set, + update, + update_pre +} from '../../src/internal/client/reactivity/sources'; import type { Derived, Effect, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; @@ -487,6 +492,26 @@ describe('signals', () => { }; }); + test('schedules rerun when updating deeply nested value', (runes) => { + if (!runes) return () => {}; + + const value = proxy({ a: { b: { c: 0 } } }); + user_effect(() => { + value.a.b.c += 1; + }); + + return () => { + let errored = false; + try { + flushSync(); + } catch (e: any) { + assert.include(e.message, 'effect_update_depth_exceeded'); + errored = true; + } + assert.equal(errored, true); + }; + }); + test('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; @@ -958,14 +983,30 @@ describe('signals', () => { }; }); - test('deriveds cannot depend on state they own', () => { + test('deriveds do not depend on state they own', () => { return () => { + let s; + const d = derived(() => { - const s = state(0); + s = state(0); return $.get(s); }); - assert.throws(() => $.get(d), 'state_unsafe_local_read'); + assert.equal($.get(d), 0); + + set(s!, 1); + assert.equal($.get(d), 0); + }; + }); + + test('effects do not depend on state they own', () => { + user_effect(() => { + const value = state(0); + set(value, $.get(value) + 1); + }); + + return () => { + flushSync(); }; }); From 1a5fb8fd51cdec1a72df9ec3100317bea83698aa Mon Sep 17 00:00:00 2001 From: Robert Gieseke Date: Fri, 21 Mar 2025 14:28:44 +0100 Subject: [PATCH 065/145] fix: Keep inlined JSDoc comments in property conversion of svelte-migrate (#15567) * Add failing JSDoc property svelte-migrate conversion tests * Add further test case and remove default value in JSDoc output * Look for inlined JSDoc comments after a hyphen * Add changeset --- .changeset/happy-cameras-bow.md | 5 +++++ packages/svelte/src/compiler/migrate/index.js | 7 +++++-- .../samples/jsdoc-with-comments/input.svelte | 9 +++++++++ .../samples/jsdoc-with-comments/output.svelte | 14 +++++++++++++- 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 .changeset/happy-cameras-bow.md diff --git a/.changeset/happy-cameras-bow.md b/.changeset/happy-cameras-bow.md new file mode 100644 index 0000000000..47188f4f6d --- /dev/null +++ b/.changeset/happy-cameras-bow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +Keep inlined trailing JSDoc comments of properties when running svelte-migrate diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 1bb7a69a20..02bb5b1443 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -1592,7 +1592,6 @@ function extract_type_and_comment(declarator, state, path) { const comment_start = /** @type {any} */ (comment_node)?.start; const comment_end = /** @type {any} */ (comment_node)?.end; let comment = comment_node && str.original.substring(comment_start, comment_end); - if (comment_node) { str.update(comment_start, comment_end, ''); } @@ -1673,6 +1672,11 @@ function extract_type_and_comment(declarator, state, path) { state.has_type_or_fallback = true; const match = /@type {(.+)}/.exec(comment_node.value); if (match) { + // try to find JSDoc comments after a hyphen `-` + const jsdocComment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value); + if (jsdocComment) { + cleaned_comment += jsdocComment[1]?.trim(); + } return { type: match[1], comment: cleaned_comment, @@ -1693,7 +1697,6 @@ function extract_type_and_comment(declarator, state, path) { }; } } - return { type: 'any', comment: state.uses_ts ? comment : cleaned_comment, diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte index f2efb1db80..f138c3a070 100644 --- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte +++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte @@ -21,6 +21,9 @@ */ export let type_no_comment; + /** @type {boolean} type_with_comment - One-line declaration with comment */ + export let type_with_comment; + /** * This is optional */ @@ -40,4 +43,10 @@ export let inline_multiline_trailing_comment = 'world'; /* * this is a same-line trailing multiline comment **/ + + /** @type {number} [default_value=1] */ + export let default_value = 1; + + /** @type {number} [comment_default_value=1] - This has a comment and an optional value. */ + export let comment_default_value = 1; \ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte index 19fbe38b50..32133ccd4c 100644 --- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte +++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte @@ -9,12 +9,18 @@ + + + + + + /** * @typedef {Object} Props * @property {string} comment - My wonderful comment @@ -22,11 +28,14 @@ * @property {any} one_line - one line comment * @property {any} no_comment * @property {boolean} type_no_comment + * @property {boolean} type_with_comment - One-line declaration with comment * @property {any} [optional] - This is optional * @property {any} inline_commented - this should stay a comment * @property {any} inline_commented_merged - This comment should be merged - with this inline comment * @property {string} [inline_multiline_leading_comment] - this is a same-line leading multiline comment * @property {string} [inline_multiline_trailing_comment] - this is a same-line trailing multiline comment + * @property {number} [default_value] + * @property {number} [comment_default_value] - This has a comment and an optional value. */ /** @type {Props} */ @@ -36,10 +45,13 @@ one_line, no_comment, type_no_comment, + type_with_comment, optional = {stuff: true}, inline_commented, inline_commented_merged, inline_multiline_leading_comment = 'world', - inline_multiline_trailing_comment = 'world' + inline_multiline_trailing_comment = 'world', + default_value = 1, + comment_default_value = 1 } = $props(); \ No newline at end of file From 1d10a65b7858ca4da8d7ade113ec5b6f9c1afb43 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 21 Mar 2025 13:30:17 +0000 Subject: [PATCH 066/145] fix: check if DOM prototypes are extensible (#15569) --- .changeset/dry-ducks-roll.md | 5 +++ .../src/internal/client/dom/operations.js | 35 +++++++++++-------- packages/svelte/src/internal/shared/utils.js | 1 + 3 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 .changeset/dry-ducks-roll.md diff --git a/.changeset/dry-ducks-roll.md b/.changeset/dry-ducks-roll.md new file mode 100644 index 0000000000..2dea8174dd --- /dev/null +++ b/.changeset/dry-ducks-roll.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: check if DOM prototypes are extensible diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 0ad9045b20..aae44d4b39 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -2,7 +2,7 @@ import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { DEV } from 'esm-env'; import { init_array_prototype_warnings } from '../dev/equality.js'; -import { get_descriptor } from '../../shared/utils.js'; +import { get_descriptor, is_extensible } from '../../shared/utils.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -34,26 +34,31 @@ export function init_operations() { var element_prototype = Element.prototype; var node_prototype = Node.prototype; + var text_prototype = Text.prototype; // @ts-ignore first_child_getter = get_descriptor(node_prototype, 'firstChild').get; // @ts-ignore next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get; - // the following assignments improve perf of lookups on DOM nodes - // @ts-expect-error - element_prototype.__click = undefined; - // @ts-expect-error - element_prototype.__className = undefined; - // @ts-expect-error - element_prototype.__attributes = null; - // @ts-expect-error - element_prototype.__style = undefined; - // @ts-expect-error - element_prototype.__e = undefined; - - // @ts-expect-error - Text.prototype.__t = undefined; + if (is_extensible(element_prototype)) { + // the following assignments improve perf of lookups on DOM nodes + // @ts-expect-error + element_prototype.__click = undefined; + // @ts-expect-error + element_prototype.__className = undefined; + // @ts-expect-error + element_prototype.__attributes = null; + // @ts-expect-error + element_prototype.__style = undefined; + // @ts-expect-error + element_prototype.__e = undefined; + } + + if (is_extensible(text_prototype)) { + // @ts-expect-error + text_prototype.__t = undefined; + } if (DEV) { // @ts-expect-error diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js index f9d52cb065..5e7f3152d8 100644 --- a/packages/svelte/src/internal/shared/utils.js +++ b/packages/svelte/src/internal/shared/utils.js @@ -10,6 +10,7 @@ export var get_descriptors = Object.getOwnPropertyDescriptors; export var object_prototype = Object.prototype; export var array_prototype = Array.prototype; export var get_prototype_of = Object.getPrototypeOf; +export var is_extensible = Object.isExtensible; /** * @param {any} thing From d2e79326c7d3810cd4ec657660d4aeec464cd689 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 09:31:03 -0400 Subject: [PATCH 067/145] fix: don't depend on deriveds created inside the current reaction (#15564) * WIP * WIP * add test * update test * changeset * oops * lint --- .changeset/young-poets-wait.md | 5 +++ packages/svelte/src/internal/client/index.js | 11 +---- packages/svelte/src/internal/client/proxy.js | 2 +- .../internal/client/reactivity/deriveds.js | 16 ++++++- .../src/internal/client/reactivity/sources.js | 24 +++++++---- .../svelte/src/internal/client/runtime.js | 43 ++++++++++++------- .../samples/effect-cleanup/_config.js | 2 +- .../samples/untrack-own-deriveds/_config.js | 20 +++++++++ .../samples/untrack-own-deriveds/main.svelte | 26 +++++++++++ packages/svelte/tests/signals/test.ts | 7 +-- 10 files changed, 114 insertions(+), 42 deletions(-) create mode 100644 .changeset/young-poets-wait.md create mode 100644 packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/main.svelte diff --git a/.changeset/young-poets-wait.md b/.changeset/young-poets-wait.md new file mode 100644 index 0000000000..479f5027ef --- /dev/null +++ b/.changeset/young-poets-wait.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't depend on deriveds created inside the current reaction diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 723ff57678..a5f93e8b17 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -101,7 +101,7 @@ export { text, props_id } from './dom/template.js'; -export { derived, derived_safe_equal } from './reactivity/deriveds.js'; +export { user_derived as derived, derived_safe_equal } from './reactivity/deriveds.js'; export { effect_tracking, effect_root, @@ -113,14 +113,7 @@ export { user_effect, user_pre_effect } from './reactivity/effects.js'; -export { - mutable_source, - mutate, - set, - source as state, - update, - update_pre -} from './reactivity/sources.js'; +export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js'; export { prop, rest_props, diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 9c3c0cf29f..ffe63f4b77 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -10,7 +10,7 @@ import { object_prototype } from '../shared/utils.js'; import { check_ownership, widen_ownership } from './dev/ownership.js'; -import { source, set } from './reactivity/sources.js'; +import { state as source, set } from './reactivity/sources.js'; import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 795417cc0f..cd7bbba02f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -8,7 +8,8 @@ import { skip_reaction, update_reaction, increment_write_version, - set_active_effect + set_active_effect, + push_reaction_value } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; @@ -61,6 +62,19 @@ export function derived(fn) { return signal; } +/** + * @template V + * @param {() => V} fn + * @returns {Derived} + */ +export function user_derived(fn) { + const d = derived(fn); + + push_reaction_value(d); + + return d; +} + /** * @template V * @param {() => V} fn diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index cac8431b4e..e4834902fe 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -15,7 +15,8 @@ import { set_reaction_sources, check_dirtiness, untracking, - is_destroying_effect + is_destroying_effect, + push_reaction_value } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -64,14 +65,6 @@ export function source(v, stack) { wv: 0 }; - if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { - if (reaction_sources === null) { - set_reaction_sources([signal]); - } else { - reaction_sources.push(signal); - } - } - if (DEV && tracing_mode_flag) { signal.created = stack ?? get_stack('CreatedAt'); signal.debug = null; @@ -80,6 +73,19 @@ export function source(v, stack) { return signal; } +/** + * @template V + * @param {V} v + * @param {Error | null} [stack] + */ +export function state(v, stack) { + const s = source(v, stack); + + push_reaction_value(s); + + return s; +} + /** * @template V * @param {V} initial_value diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 74b58ee1a9..a5d26412a4 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -101,6 +101,17 @@ export function set_reaction_sources(sources) { reaction_sources = sources; } +/** @param {Value} value */ +export function push_reaction_value(value) { + if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { + if (reaction_sources === null) { + set_reaction_sources([value]); + } else { + reaction_sources.push(value); + } + } +} + /** * The dependencies of the reaction that is currently being executed. In many cases, * the dependencies are unchanged between runs, and so this will be `null` unless @@ -875,21 +886,23 @@ export function get(signal) { // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { - var deps = active_reaction.deps; - if (signal.rv < read_version) { - signal.rv = read_version; - // If the signal is accessing the same dependencies in the same - // order as it did last time, increment `skipped_deps` - // rather than updating `new_deps`, which creates GC cost - if (new_deps === null && deps !== null && deps[skipped_deps] === signal) { - skipped_deps++; - } else if (new_deps === null) { - new_deps = [signal]; - } else if (!skip_reaction || !new_deps.includes(signal)) { - // Normally we can push duplicated dependencies to `new_deps`, but if we're inside - // an unowned derived because skip_reaction is true, then we need to ensure that - // we don't have duplicates - new_deps.push(signal); + if (!reaction_sources?.includes(signal)) { + var deps = active_reaction.deps; + if (signal.rv < read_version) { + signal.rv = read_version; + // If the signal is accessing the same dependencies in the same + // order as it did last time, increment `skipped_deps` + // rather than updating `new_deps`, which creates GC cost + if (new_deps === null && deps !== null && deps[skipped_deps] === signal) { + skipped_deps++; + } else if (new_deps === null) { + new_deps = [signal]; + } else if (!skip_reaction || !new_deps.includes(signal)) { + // Normally we can push duplicated dependencies to `new_deps`, but if we're inside + // an unowned derived because skip_reaction is true, then we need to ensure that + // we don't have duplicates + new_deps.push(signal); + } } } } else if ( diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js index 6a3d9eef77..e55733c148 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js @@ -10,6 +10,6 @@ export default test({ flushSync(() => { b1.click(); }); - assert.deepEqual(logs, ['init 0', 'cleanup 2', null, 'init 2', 'cleanup 4', null, 'init 4']); + assert.deepEqual(logs, ['init 0']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js new file mode 100644 index 0000000000..18062b86fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + + assert.htmlEqual( + target.innerHTML, + ` + +

1/2

+ class Foo { + value = $state(0); + double = $derived(this.value * 2); + + constructor() { + console.log(this.value, this.double); + } + + increment() { + this.value++; + } + } + + let foo = $state(); + + $effect(() => { + foo = new Foo(); + }); + + + + +{#if foo} +

{foo.value}/{foo.double}

+{/if} diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 72f99c90e5..3977caae36 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -8,12 +8,7 @@ import { render_effect, user_effect } from '../../src/internal/client/reactivity/effects'; -import { - source as state, - set, - update, - update_pre -} from '../../src/internal/client/reactivity/sources'; +import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; import type { Derived, Effect, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; From e25c2812961f9bb74ab50f1b034d8e5a5d8ae412 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 09:40:37 -0400 Subject: [PATCH 068/145] Version Packages (#15551) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/dirty-pianos-sparkle.md | 5 ----- .changeset/dry-ducks-roll.md | 5 ----- .changeset/happy-cameras-bow.md | 5 ----- .changeset/nine-laws-rush.md | 5 ----- .changeset/young-poets-wait.md | 5 ----- packages/svelte/CHANGELOG.md | 16 ++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 18 insertions(+), 27 deletions(-) delete mode 100644 .changeset/dirty-pianos-sparkle.md delete mode 100644 .changeset/dry-ducks-roll.md delete mode 100644 .changeset/happy-cameras-bow.md delete mode 100644 .changeset/nine-laws-rush.md delete mode 100644 .changeset/young-poets-wait.md diff --git a/.changeset/dirty-pianos-sparkle.md b/.changeset/dirty-pianos-sparkle.md deleted file mode 100644 index b3e4dd1d8c..0000000000 --- a/.changeset/dirty-pianos-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: allow state created in deriveds/effects to be written/read locally without self-invalidation diff --git a/.changeset/dry-ducks-roll.md b/.changeset/dry-ducks-roll.md deleted file mode 100644 index 2dea8174dd..0000000000 --- a/.changeset/dry-ducks-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: check if DOM prototypes are extensible diff --git a/.changeset/happy-cameras-bow.md b/.changeset/happy-cameras-bow.md deleted file mode 100644 index 47188f4f6d..0000000000 --- a/.changeset/happy-cameras-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -Keep inlined trailing JSDoc comments of properties when running svelte-migrate diff --git a/.changeset/nine-laws-rush.md b/.changeset/nine-laws-rush.md deleted file mode 100644 index e0a0fc15a0..0000000000 --- a/.changeset/nine-laws-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: simplify set calls for proxyable values diff --git a/.changeset/young-poets-wait.md b/.changeset/young-poets-wait.md deleted file mode 100644 index 479f5027ef..0000000000 --- a/.changeset/young-poets-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't depend on deriveds created inside the current reaction diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6461df1d25..8cb7efd4ef 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,21 @@ # svelte +## 5.24.0 + +### Minor Changes + +- feat: allow state created in deriveds/effects to be written/read locally without self-invalidation ([#15553](https://github.com/sveltejs/svelte/pull/15553)) + +### Patch Changes + +- fix: check if DOM prototypes are extensible ([#15569](https://github.com/sveltejs/svelte/pull/15569)) + +- Keep inlined trailing JSDoc comments of properties when running svelte-migrate ([#15567](https://github.com/sveltejs/svelte/pull/15567)) + +- fix: simplify set calls for proxyable values ([#15548](https://github.com/sveltejs/svelte/pull/15548)) + +- fix: don't depend on deriveds created inside the current reaction ([#15564](https://github.com/sveltejs/svelte/pull/15564)) + ## 5.23.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d005eca0b9..0aa6b29841 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.23.2", + "version": "5.24.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 191b52ecef..7cd43e74cb 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.23.2'; +export const VERSION = '5.24.0'; export const PUBLIC_VERSION = '5'; From 6b23a7c4777a123dc1ea4db6cb87e03268fbb45b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 09:52:30 -0400 Subject: [PATCH 069/145] chore: camelCase -> snake_case (#15573) --- packages/svelte/src/compiler/migrate/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 02bb5b1443..7f26d0d010 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -1673,9 +1673,9 @@ function extract_type_and_comment(declarator, state, path) { const match = /@type {(.+)}/.exec(comment_node.value); if (match) { // try to find JSDoc comments after a hyphen `-` - const jsdocComment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value); - if (jsdocComment) { - cleaned_comment += jsdocComment[1]?.trim(); + const jsdoc_comment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value); + if (jsdoc_comment) { + cleaned_comment += jsdoc_comment[1]?.trim(); } return { type: match[1], From 83d0c5894dc26c92274f162c9f9495038cabe37d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 09:52:51 -0400 Subject: [PATCH 070/145] docs: add note on effect-local state (#15572) --- documentation/docs/02-runes/04-$effect.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 6a2b565aea..75f59102f9 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -74,6 +74,8 @@ Teardown functions also run when the effect is destroyed, which happens when its `$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a re-run. +If `$state` and `$derived` are used directly inside the `$effect` (for example, during creation of a [reactive class](https://svelte.dev/docs/svelte/$state#Classes)), those values will _not_ be treated as dependencies. + Values that are read _asynchronously_ — after an `await` or inside a `setTimeout`, for example — will not be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/playground/untitled#H4sIAAAAAAAAE31T246bMBD9lZF3pWSlBEirfaEQqdo_2PatVIpjBrDkGGQPJGnEv1e2IZfVal-wfHzmzJyZ4cIqqdCy9M-F0blDlnqArZjmB3f72XWRHVCRw_bc4me4aDWhJstSlllhZEfbQhekkMDKfwg5PFvihMvX5OXH_CJa1Zrb0-Kpqr5jkiwC48rieuDWQbqgZ6wqFLRcvkC-hYvnkWi1dWqa8ESQTxFRjfQWsOXiWzmr0sSLhEJu3p1YsoJkNUcdZUnN9dagrBu6FVRQHAM10sJRKgUG16bXcGxQ44AGdt7SDkTDdY02iqLHnJVU6hedlWuIp94JW6Tf8oBt_8GdTxlF0b4n0C35ZLBzXb3mmYn3ae6cOW74zj0YVzDNYXRHFt9mprNgHfZSl6mzml8CMoLvTV6wTZIUDEJv5us2iwMtiJRyAKG4tXnhl8O0yhbML0Wm-B7VNlSSSd31BG7z8oIZZ6dgIffAVY_5xdU9Qrz1Bnx8fCfwtZ7v8Qc9j3nB8PqgmMWlHIID6-bkVaPZwDySfWtKNGtquxQ23Qlsq2QJT0KIqb8dL0up6xQ2eIBkAg_c1FI_YqW0neLnFCqFpwmreedJYT7XX8FVOBfwWRhXstZrSXiwKQjUhOZeMIleb5JZfHWn2Yq5pWEpmR7Hv-N_wEqT8hEEAAA=)): ```ts From ade66c6feade92cfd932dcb4be2812305e518d2b Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 21 Mar 2025 15:21:40 +0100 Subject: [PATCH 071/145] fix: use `get` in constructor for deriveds (#15300) Co-authored-by: Rich Harris --- .changeset/new-cherries-leave.md | 5 +++++ .../client/visitors/MemberExpression.js | 4 +++- .../samples/deriveds-in-constructor/_config.js | 5 +++++ .../deriveds-in-constructor/main.svelte | 18 ++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/new-cherries-leave.md create mode 100644 packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte diff --git a/.changeset/new-cherries-leave.md b/.changeset/new-cherries-leave.md new file mode 100644 index 0000000000..738a78b4a3 --- /dev/null +++ b/.changeset/new-cherries-leave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use `get` in constructor for deriveds diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js index 501ecda555..3f2aada1f5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js @@ -11,7 +11,9 @@ export function MemberExpression(node, context) { if (node.property.type === 'PrivateIdentifier') { const field = context.state.private_state.get(node.property.name); if (field) { - return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node); + return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === 'state') + ? b.member(node, 'v') + : b.call('$.get', node); } } diff --git a/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js new file mode 100644 index 0000000000..b364a989f4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

state,derived state,derived.by derived state

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte new file mode 100644 index 0000000000..bc8efba7e7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte @@ -0,0 +1,18 @@ + + +

{foo.initial}

\ No newline at end of file From 1f37c02f918d6fa4d8a14de5d6868228e61dd05a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 21 Mar 2025 14:25:46 +0000 Subject: [PATCH 072/145] fix: ensure toStore root effect is connected to correct parent effect (#15574) * fix: ensure toStore root effect is connected to correct parent effect * prettier --------- Co-authored-by: Rich Harris --- .changeset/twelve-bananas-destroy.md | 5 +++ packages/svelte/src/store/index-client.js | 35 +++++++++++++++---- .../samples/toStore-subscribe2/_config.js | 16 +++++++++ .../samples/toStore-subscribe2/main.svelte | 11 ++++++ 4 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 .changeset/twelve-bananas-destroy.md create mode 100644 packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte diff --git a/.changeset/twelve-bananas-destroy.md b/.changeset/twelve-bananas-destroy.md new file mode 100644 index 0000000000..873ee21877 --- /dev/null +++ b/.changeset/twelve-bananas-destroy.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure toStore root effect is connected to correct parent effect diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js index ae6806ec76..2f0a1a831a 100644 --- a/packages/svelte/src/store/index-client.js +++ b/packages/svelte/src/store/index-client.js @@ -6,6 +6,12 @@ import { } from '../internal/client/reactivity/effects.js'; import { get, writable } from './shared/index.js'; import { createSubscriber } from '../reactivity/create-subscriber.js'; +import { + active_effect, + active_reaction, + set_active_effect, + set_active_reaction +} from '../internal/client/runtime.js'; export { derived, get, readable, readonly, writable } from './shared/index.js'; @@ -39,19 +45,34 @@ export { derived, get, readable, readonly, writable } from './shared/index.js'; * @returns {Writable | Readable} */ export function toStore(get, set) { - let init_value = get(); + var effect = active_effect; + var reaction = active_reaction; + var init_value = get(); + const store = writable(init_value, (set) => { // If the value has changed before we call subscribe, then // we need to treat the value as already having run - let ran = init_value !== get(); + var ran = init_value !== get(); // TODO do we need a different implementation on the server? - const teardown = effect_root(() => { - render_effect(() => { - const value = get(); - if (ran) set(value); + var teardown; + // Apply the reaction and effect at the time of toStore being called + var previous_reaction = active_reaction; + var previous_effect = active_effect; + set_active_reaction(reaction); + set_active_effect(effect); + + try { + teardown = effect_root(() => { + render_effect(() => { + const value = get(); + if (ran) set(value); + }); }); - }); + } finally { + set_active_reaction(previous_reaction); + set_active_effect(previous_effect); + } ran = true; diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js new file mode 100644 index 0000000000..bc1793e7a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + let btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + `
Count 1!
Count from store 1!
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte new file mode 100644 index 0000000000..82d20105b8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte @@ -0,0 +1,11 @@ + + +
Count {counter}!
+
Count from store {$count}!
+ + From 842a7c6995f94f46b1839fcac91042fd541e52ca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 10:26:15 -0400 Subject: [PATCH 073/145] docs: update state_unsafe_mutation message (#15539) * docs: update state_unsafe_mutation message * regenerate * fix example --- .../98-reference/.generated/client-errors.md | 35 +++++++++++-------- .../svelte/messages/client-errors/errors.md | 35 +++++++++++-------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 62d9c3302a..901c49822c 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -128,26 +128,31 @@ Cannot set prototype of `$state` object Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` ``` -This error is thrown in a situation like this: +This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: ```svelte - + + +

{count} is even: {even}

+

{count} is odd: {odd}

``` -Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. +This is forbidden because it introduces instability: if `

{count} is even: {even}

` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: + +```js +let even = $derived(count % 2 === 0); +let odd = $derived(!even); +``` -To fix this: -- See if it's possible to refactor your `$derived` such that the update becomes unnecessary -- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update -- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates +If side-effects are unavoidable, use [`$effect`]($effect) instead. diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index bc8ec36256..572930843e 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -84,26 +84,31 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` -This error is thrown in a situation like this: +This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: ```svelte - + + +

{count} is even: {even}

+

{count} is odd: {odd}

``` -Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. +This is forbidden because it introduces instability: if `

{count} is even: {even}

` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: + +```js +let even = $derived(count % 2 === 0); +let odd = $derived(!even); +``` -To fix this: -- See if it's possible to refactor your `$derived` such that the update becomes unnecessary -- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update -- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates +If side-effects are unavoidable, use [`$effect`]($effect) instead. From 2d3b65dfbd589416d94661bd34ed7a99896bcde2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:28:23 -0400 Subject: [PATCH 074/145] Version Packages (#15575) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/new-cherries-leave.md | 5 ----- .changeset/twelve-bananas-destroy.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/new-cherries-leave.md delete mode 100644 .changeset/twelve-bananas-destroy.md diff --git a/.changeset/new-cherries-leave.md b/.changeset/new-cherries-leave.md deleted file mode 100644 index 738a78b4a3..0000000000 --- a/.changeset/new-cherries-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: use `get` in constructor for deriveds diff --git a/.changeset/twelve-bananas-destroy.md b/.changeset/twelve-bananas-destroy.md deleted file mode 100644 index 873ee21877..0000000000 --- a/.changeset/twelve-bananas-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure toStore root effect is connected to correct parent effect diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8cb7efd4ef..04ddfcadbd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.24.1 + +### Patch Changes + +- fix: use `get` in constructor for deriveds ([#15300](https://github.com/sveltejs/svelte/pull/15300)) + +- fix: ensure toStore root effect is connected to correct parent effect ([#15574](https://github.com/sveltejs/svelte/pull/15574)) + ## 5.24.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0aa6b29841..f321571e7a 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.24.0", + "version": "5.24.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 7cd43e74cb..565c190713 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.24.0'; +export const VERSION = '5.24.1'; export const PUBLIC_VERSION = '5'; From 5a8fa69dbf46e99beed812157ed78609f8054331 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 12:54:53 -0400 Subject: [PATCH 075/145] feat: make deriveds writable (#15570) * feat: make deriveds writable * add optimistic UI example * add note to when-not-to-use-effect * add section on deep reactivity * root-relative URL * use hash URL * mention const * make handler async, move into script block --- .changeset/clever-terms-tell.md | 5 ++ documentation/docs/02-runes/03-$derived.md | 42 +++++++++++++++++ documentation/docs/02-runes/04-$effect.md | 2 + .../phases/2-analyze/visitors/shared/utils.js | 31 +------------ .../runes-no-derived-assignment/_config.js | 8 ---- .../runes-no-derived-assignment/main.svelte | 5 -- .../runes-no-derived-binding/_config.js | 8 ---- .../runes-no-derived-binding/main.svelte | 6 --- .../_config.js | 8 ---- .../main.svelte | 10 ---- .../_config.js | 8 ---- .../main.svelte | 10 ---- .../runes-no-derived-update/_config.js | 8 ---- .../runes-no-derived-update/main.svelte | 5 -- .../samples/writable-derived/_config.js | 46 +++++++++++++++++++ .../samples/writable-derived/main.svelte | 9 ++++ .../reassign-derived-literal/errors.json | 14 ------ .../reassign-derived-literal/input.svelte | 9 ---- .../errors.json | 14 ------ .../input.svelte | 9 ---- .../reassign-derived-public-field/errors.json | 14 ------ .../input.svelte | 9 ---- 22 files changed, 105 insertions(+), 175 deletions(-) create mode 100644 .changeset/clever-terms-tell.md delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte diff --git a/.changeset/clever-terms-tell.md b/.changeset/clever-terms-tell.md new file mode 100644 index 0000000000..606868bce3 --- /dev/null +++ b/.changeset/clever-terms-tell.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: make deriveds writable diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 24ab643b68..2464aa9295 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -52,6 +52,48 @@ Anything read synchronously inside the `$derived` expression (or `$derived.by` f To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack). +## Overriding derived values + +Derived expressions are recalculated when their dependencies change, but you can temporarily override their values by reassigning them (unless they are declared with `const`). This can be useful for things like _optimistic UI_, where a value is derived from the 'source of truth' (such as data from your server) but you'd like to show immediate feedback to the user: + +```svelte + + + +``` + +> [!NOTE] Prior to Svelte 5.25, deriveds were read-only. + +## Deriveds and reactivity + +Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)... + +```svelte +let items = $state([...]); + +let index = $state(0); +let selected = $derived(items[index]); +``` + +...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect. + ## Update propagation Svelte uses something called _push-pull reactivity_ — when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull'). diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 75f59102f9..ae1a2146c9 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -254,6 +254,8 @@ In general, `$effect` is best considered something of an escape hatch — useful > [!NOTE] For things that are more complicated than a simple expression like `count * 2`, you can also use `$derived.by`. +If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25. + You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)): ```svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 04f4347a40..d6c74eddb6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -21,10 +21,6 @@ export function validate_assignment(node, argument, state) { const binding = state.scope.get(argument.name); if (state.analysis.runes) { - if (binding?.kind === 'derived') { - e.constant_assignment(node, 'derived state'); - } - if (binding?.node === state.analysis.props_id) { e.constant_assignment(node, '$props.id()'); } @@ -38,25 +34,6 @@ export function validate_assignment(node, argument, state) { e.snippet_parameter_assignment(node); } } - if ( - argument.type === 'MemberExpression' && - argument.object.type === 'ThisExpression' && - (((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') && - state.derived_state.some( - (derived) => - derived.name === /** @type {PrivateIdentifier | Identifier} */ (argument.property).name && - derived.private === (argument.property.type === 'PrivateIdentifier') - )) || - (argument.property.type === 'Literal' && - argument.property.value && - typeof argument.property.value === 'string' && - state.derived_state.some( - (derived) => - derived.name === /** @type {Literal} */ (argument.property).value && !derived.private - ))) - ) { - e.constant_assignment(node, 'derived state'); - } } /** @@ -81,7 +58,6 @@ export function validate_no_const_assignment(node, argument, scope, is_binding) } else if (argument.type === 'Identifier') { const binding = scope.get(argument.name); if ( - binding?.kind === 'derived' || binding?.declaration_kind === 'import' || (binding?.declaration_kind === 'const' && binding.kind !== 'each') ) { @@ -96,12 +72,7 @@ export function validate_no_const_assignment(node, argument, scope, is_binding) // ); // TODO have a more specific error message for assignments to things like `{:then foo}` - const thing = - binding.declaration_kind === 'import' - ? 'import' - : binding.kind === 'derived' - ? 'derived state' - : 'constant'; + const thing = binding.declaration_kind === 'import' ? 'import' : 'constant'; if (is_binding) { e.constant_binding(node, thing); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js deleted file mode 100644 index 94985a9939..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_assignment', - message: 'Cannot assign to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte deleted file mode 100644 index 3bf836f6c5..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js deleted file mode 100644 index 87b88d79cc..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_binding', - message: 'Cannot bind to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte deleted file mode 100644 index 6c198dc068..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js deleted file mode 100644 index 94985a9939..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_assignment', - message: 'Cannot assign to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte deleted file mode 100644 index d44806757e..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js deleted file mode 100644 index 94985a9939..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_assignment', - message: 'Cannot assign to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte deleted file mode 100644 index e4ee2e8635..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js deleted file mode 100644 index 94985a9939..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_assignment', - message: 'Cannot assign to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte deleted file mode 100644 index d266c95bb8..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js new file mode 100644 index 0000000000..b48ccbdfd0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js @@ -0,0 +1,46 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` +

0 * 2 = 0

+ `, + + ssrHtml: ` +

0 * 2 = 0

+ `, + + test({ assert, target, window }) { + const [input1, input2] = target.querySelectorAll('input'); + + flushSync(() => { + input1.value = '10'; + input1.dispatchEvent(new window.Event('input', { bubbles: true })); + }); + + assert.htmlEqual( + target.innerHTML, + `

10 * 2 = 20

` + ); + + flushSync(() => { + input2.value = '99'; + input2.dispatchEvent(new window.Event('input', { bubbles: true })); + }); + + assert.htmlEqual( + target.innerHTML, + `

10 * 2 = 99

` + ); + + flushSync(() => { + input1.value = '20'; + input1.dispatchEvent(new window.Event('input', { bubbles: true })); + }); + + assert.htmlEqual( + target.innerHTML, + `

20 * 2 = 40

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

{count} * 2 = {double}

diff --git a/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json deleted file mode 100644 index 8681d84ab2..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "code": "constant_assignment", - "message": "Cannot assign to derived state", - "start": { - "column": 3, - "line": 6 - }, - "end": { - "column": 29, - "line": 6 - } - } -] diff --git a/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte deleted file mode 100644 index 8f109c9e1f..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json deleted file mode 100644 index c211aa4608..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "code": "constant_assignment", - "message": "Cannot assign to derived state", - "start": { - "column": 3, - "line": 6 - }, - "end": { - "column": 27, - "line": 6 - } - } -] diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte deleted file mode 100644 index 62e2317e03..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json deleted file mode 100644 index 98837589ac..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "code": "constant_assignment", - "message": "Cannot assign to derived state", - "start": { - "column": 3, - "line": 6 - }, - "end": { - "column": 26, - "line": 6 - } - } -] diff --git a/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte deleted file mode 100644 index e2c4693e86..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file From 6e343b9ad7bca473947cbee0c7ea9455d9485599 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:01:58 -0400 Subject: [PATCH 076/145] Version Packages (#15578) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clever-terms-tell.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/clever-terms-tell.md diff --git a/.changeset/clever-terms-tell.md b/.changeset/clever-terms-tell.md deleted file mode 100644 index 606868bce3..0000000000 --- a/.changeset/clever-terms-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: make deriveds writable diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 04ddfcadbd..4bac129169 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.25.0 + +### Minor Changes + +- feat: make deriveds writable ([#15570](https://github.com/sveltejs/svelte/pull/15570)) + ## 5.24.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f321571e7a..e3824b89fb 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.24.1", + "version": "5.25.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 565c190713..a62190bb2e 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.24.1'; +export const VERSION = '5.25.0'; export const PUBLIC_VERSION = '5'; From 441108b8ff28a6c1aa8e38f5c041a2583446167e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 13:06:46 -0400 Subject: [PATCH 077/145] fix docs --- documentation/docs/98-reference/.generated/client-errors.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 901c49822c..fd9419176d 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -151,6 +151,8 @@ This error occurs when state is updated while evaluating a `$derived`. You might This is forbidden because it introduces instability: if `

{count} is even: {even}

` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: ```js +let count = 0; +// ---cut--- let even = $derived(count % 2 === 0); let odd = $derived(!even); ``` From ef98ccae8b27dbac393623c166ea890b515d5e1d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 15:44:23 -0400 Subject: [PATCH 078/145] doh --- documentation/docs/98-reference/.generated/client-errors.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index fd9419176d..901c49822c 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -151,8 +151,6 @@ This error occurs when state is updated while evaluating a `$derived`. You might This is forbidden because it introduces instability: if `

{count} is even: {even}

` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: ```js -let count = 0; -// ---cut--- let even = $derived(count % 2 === 0); let odd = $derived(!even); ``` From d1bd32ec9ec06e6505740a0de5f5b2281546787c Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 21 Mar 2025 14:46:20 -0500 Subject: [PATCH 079/145] fix: allow get_proxied_value to return original value when error (#15577) * fix: allow get_proxied_value to return original value when error closes #15546 * Update packages/svelte/src/internal/client/proxy.js --------- Co-authored-by: Rich Harris --- .changeset/afraid-penguins-battle.md | 5 +++++ packages/svelte/src/internal/client/proxy.js | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .changeset/afraid-penguins-battle.md diff --git a/.changeset/afraid-penguins-battle.md b/.changeset/afraid-penguins-battle.md new file mode 100644 index 0000000000..2cc5059b9a --- /dev/null +++ b/.changeset/afraid-penguins-battle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent dev server from throwing errors when attempting to retrieve the proxied value of an iframe's contentWindow diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index ffe63f4b77..fab271c916 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -366,8 +366,18 @@ function update_version(signal, d = 1) { * @param {any} value */ export function get_proxied_value(value) { - if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) { - return value[STATE_SYMBOL]; + try { + if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) { + return value[STATE_SYMBOL]; + } + } catch { + // the above if check can throw an error if the value in question + // is the contentWindow of an iframe on another domain, in which + // case we want to just return the value (because it's definitely + // not a proxied value) so we don't break any JavaScript interacting + // with that iframe (such as various payment companies client side + // JavaScript libraries interacting with their iframes on the same + // domain) } return value; From c1ae8953aaa81b9191d8d944c4bf0df7fdf4f2ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:04:59 -0400 Subject: [PATCH 080/145] Version Packages (#15580) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/afraid-penguins-battle.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/afraid-penguins-battle.md diff --git a/.changeset/afraid-penguins-battle.md b/.changeset/afraid-penguins-battle.md deleted file mode 100644 index 2cc5059b9a..0000000000 --- a/.changeset/afraid-penguins-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent dev server from throwing errors when attempting to retrieve the proxied value of an iframe's contentWindow diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4bac129169..9e99e91b8e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.25.1 + +### Patch Changes + +- fix: prevent dev server from throwing errors when attempting to retrieve the proxied value of an iframe's contentWindow ([#15577](https://github.com/sveltejs/svelte/pull/15577)) + ## 5.25.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e3824b89fb..9d3902696d 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.25.0", + "version": "5.25.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a62190bb2e..a4f5a15c8f 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.25.0'; +export const VERSION = '5.25.1'; export const PUBLIC_VERSION = '5'; From 33d118f8a29a376e4490f2d31b0b444bf8fa0c7c Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 21 Mar 2025 23:47:26 +0100 Subject: [PATCH 081/145] feat: migrate reassigned deriveds to `$derived` (#15581) --- .changeset/forty-snakes-lay.md | 5 +++++ packages/svelte/src/compiler/migrate/index.js | 17 ++++++++++++++++- .../samples/reassigned-deriveds/input.svelte | 10 ++++++++++ .../samples/reassigned-deriveds/output.svelte | 10 ++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 .changeset/forty-snakes-lay.md create mode 100644 packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte create mode 100644 packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte diff --git a/.changeset/forty-snakes-lay.md b/.changeset/forty-snakes-lay.md new file mode 100644 index 0000000000..6cb4c2d761 --- /dev/null +++ b/.changeset/forty-snakes-lay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: migrate reassigned deriveds to `$derived` diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 7f26d0d010..b336ebb2b8 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -19,6 +19,7 @@ import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js'; import { validate_component_options } from '../validate-options.js'; import { is_reserved, is_svg, is_void } from '../../utils.js'; import { regex_is_valid_identifier } from '../phases/patterns.js'; +import { VERSION } from 'svelte/compiler'; const regex_style_tags = /(]+>)([\S\s]*?)(<\/style>)/g; const style_placeholder = '/*$$__STYLE_CONTENT__$$*/'; @@ -113,6 +114,16 @@ function find_closing_parenthesis(start, code) { return end; } +function check_support_writable_deriveds() { + const [major, minor, patch] = VERSION.split('.'); + + if (+major < 5) return false; + if (+minor < 25) return false; + return true; +} + +const support_writable_derived = check_support_writable_deriveds(); + /** * Does a best-effort migration of Svelte code towards using runes, event attributes and render tags. * May throw an error if the code is too complex to migrate automatically. @@ -952,7 +963,11 @@ const instance_script = { const reassigned_bindings = bindings.filter((b) => b?.reassigned); if ( - reassigned_bindings.length === 0 && + // on version 5.25.0 deriveds are writable so we can use them even if + // reassigned (but if the right side is a literal we want to use `$state`) + (support_writable_derived + ? node.body.expression.right.type !== 'Literal' + : reassigned_bindings.length === 0) && !bindings.some((b) => b?.kind === 'store_sub') && node.body.expression.left.type !== 'MemberExpression' ) { diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte new file mode 100644 index 0000000000..024f719fb9 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte @@ -0,0 +1,10 @@ + + + + + +{upper} \ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte new file mode 100644 index 0000000000..0903299d95 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte @@ -0,0 +1,10 @@ + + + + + +{upper} \ No newline at end of file From 78d238c5a34396afcc9edf02bb767d0936440ad9 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 22 Mar 2025 00:03:38 +0100 Subject: [PATCH 082/145] chore: revert version check in migrate (#15583) --- packages/svelte/src/compiler/migrate/index.js | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index b336ebb2b8..9d79d88b23 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -19,7 +19,6 @@ import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js'; import { validate_component_options } from '../validate-options.js'; import { is_reserved, is_svg, is_void } from '../../utils.js'; import { regex_is_valid_identifier } from '../phases/patterns.js'; -import { VERSION } from 'svelte/compiler'; const regex_style_tags = /(]+>)([\S\s]*?)(<\/style>)/g; const style_placeholder = '/*$$__STYLE_CONTENT__$$*/'; @@ -114,16 +113,6 @@ function find_closing_parenthesis(start, code) { return end; } -function check_support_writable_deriveds() { - const [major, minor, patch] = VERSION.split('.'); - - if (+major < 5) return false; - if (+minor < 25) return false; - return true; -} - -const support_writable_derived = check_support_writable_deriveds(); - /** * Does a best-effort migration of Svelte code towards using runes, event attributes and render tags. * May throw an error if the code is too complex to migrate automatically. @@ -963,11 +952,7 @@ const instance_script = { const reassigned_bindings = bindings.filter((b) => b?.reassigned); if ( - // on version 5.25.0 deriveds are writable so we can use them even if - // reassigned (but if the right side is a literal we want to use `$state`) - (support_writable_derived - ? node.body.expression.right.type !== 'Literal' - : reassigned_bindings.length === 0) && + node.body.expression.right.type !== 'Literal' && !bindings.some((b) => b?.kind === 'store_sub') && node.body.expression.left.type !== 'MemberExpression' ) { From 7fe9bf524bbf060a0acf8b91c8ca322b105423b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 22 Mar 2025 00:04:01 +0100 Subject: [PATCH 083/145] Version Packages (#15582) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/forty-snakes-lay.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/forty-snakes-lay.md diff --git a/.changeset/forty-snakes-lay.md b/.changeset/forty-snakes-lay.md deleted file mode 100644 index 6cb4c2d761..0000000000 --- a/.changeset/forty-snakes-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -feat: migrate reassigned deriveds to `$derived` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 9e99e91b8e..86e1179029 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.25.2 + +### Patch Changes + +- feat: migrate reassigned deriveds to `$derived` ([#15581](https://github.com/sveltejs/svelte/pull/15581)) + ## 5.25.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 9d3902696d..56fa949391 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.25.1", + "version": "5.25.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a4f5a15c8f..e2dc6c2c09 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.25.1'; +export const VERSION = '5.25.2'; export const PUBLIC_VERSION = '5'; From 3080c1334e8efc65756488c0cc36b6dec5fca00f Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 22 Mar 2025 01:56:31 +0100 Subject: [PATCH 084/145] fix: prevent state runes from being called with spread (#15585) * fix: prevent state runes from being called with spread * prevent spread arguments for all runes except $inspect --------- Co-authored-by: Rich Harris --- .changeset/nice-pianos-punch.md | 5 +++++ .../docs/98-reference/.generated/compile-errors.md | 6 ++++++ packages/svelte/messages/compile-errors/script.md | 4 ++++ packages/svelte/src/compiler/errors.js | 10 ++++++++++ .../phases/2-analyze/visitors/CallExpression.js | 8 ++++++++ .../rune-invalid-spread-derived-by/errors.json | 14 ++++++++++++++ .../rune-invalid-spread-derived-by/input.svelte | 4 ++++ .../rune-invalid-spread-derived/errors.json | 14 ++++++++++++++ .../rune-invalid-spread-derived/input.svelte | 4 ++++ .../rune-invalid-spread-state-raw/errors.json | 14 ++++++++++++++ .../rune-invalid-spread-state-raw/input.svelte | 4 ++++ .../samples/rune-invalid-spread-state/errors.json | 14 ++++++++++++++ .../samples/rune-invalid-spread-state/input.svelte | 4 ++++ 13 files changed, 105 insertions(+) create mode 100644 .changeset/nice-pianos-punch.md create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte diff --git a/.changeset/nice-pianos-punch.md b/.changeset/nice-pianos-punch.md new file mode 100644 index 0000000000..f70af9eb04 --- /dev/null +++ b/.changeset/nice-pianos-punch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent state runes from being called with spread diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index ea116014e7..a8c39aaf97 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -660,6 +660,12 @@ Cannot access a computed property of a rune `%name%` is not a valid rune ``` +### rune_invalid_spread + +``` +`%rune%` cannot be called with a spread argument +``` + ### rune_invalid_usage ``` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 795c0b007d..aabcbeae48 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -162,6 +162,10 @@ This turned out to be buggy and unpredictable, particularly when working with de > `%name%` is not a valid rune +## rune_invalid_spread + +> `%rune%` cannot be called with a spread argument + ## rune_invalid_usage > Cannot use `%rune%` rune in non-runes mode diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 677b99fcff..6bf973948b 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -383,6 +383,16 @@ export function rune_invalid_name(node, name) { e(node, 'rune_invalid_name', `\`${name}\` is not a valid rune\nhttps://svelte.dev/e/rune_invalid_name`); } +/** + * `%rune%` cannot be called with a spread argument + * @param {null | number | NodeLike} node + * @param {string} rune + * @returns {never} + */ +export function rune_invalid_spread(node, rune) { + e(node, 'rune_invalid_spread', `\`${rune}\` cannot be called with a spread argument\nhttps://svelte.dev/e/rune_invalid_spread`); +} + /** * Cannot use `%rune%` rune in non-runes mode * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 6ef323725b..2eac934b33 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -17,6 +17,14 @@ export function CallExpression(node, context) { const rune = get_rune(node, context.state.scope); + if (rune && rune !== '$inspect') { + for (const arg of node.arguments) { + if (arg.type === 'SpreadElement') { + e.rune_invalid_spread(node, rune); + } + } + } + switch (rune) { case null: if (!is_safe_identifier(node.callee, context.state.scope)) { diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json new file mode 100644 index 0000000000..be59da95fa --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "rune_invalid_spread", + "end": { + "column": 35, + "line": 3 + }, + "message": "`$derived.by` cannot be called with a spread argument", + "start": { + "column": 15, + "line": 3 + } + } +] diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte new file mode 100644 index 0000000000..49e8057aa5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json new file mode 100644 index 0000000000..6a333bc362 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "rune_invalid_spread", + "end": { + "column": 32, + "line": 3 + }, + "message": "`$derived` cannot be called with a spread argument", + "start": { + "column": 15, + "line": 3 + } + } +] diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte new file mode 100644 index 0000000000..9155493e17 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json new file mode 100644 index 0000000000..e08b498fcb --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "rune_invalid_spread", + "end": { + "column": 34, + "line": 3 + }, + "message": "`$state.raw` cannot be called with a spread argument", + "start": { + "column": 15, + "line": 3 + } + } +] diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte new file mode 100644 index 0000000000..d06fb053b3 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json new file mode 100644 index 0000000000..11ae2abce5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "rune_invalid_spread", + "end": { + "column": 30, + "line": 3 + }, + "message": "`$state` cannot be called with a spread argument", + "start": { + "column": 15, + "line": 3 + } + } +] diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte new file mode 100644 index 0000000000..02feac893f --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file From f498a21063894e6e515e62d753396410624b2e0f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 09:59:21 -0400 Subject: [PATCH 085/145] Version Packages (#15587) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/nice-pianos-punch.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/nice-pianos-punch.md diff --git a/.changeset/nice-pianos-punch.md b/.changeset/nice-pianos-punch.md deleted file mode 100644 index f70af9eb04..0000000000 --- a/.changeset/nice-pianos-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent state runes from being called with spread diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 86e1179029..e6b5de7e5f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.25.3 + +### Patch Changes + +- fix: prevent state runes from being called with spread ([#15585](https://github.com/sveltejs/svelte/pull/15585)) + ## 5.25.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 56fa949391..0410520634 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.25.2", + "version": "5.25.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e2dc6c2c09..e4ae33a9b1 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.25.2'; +export const VERSION = '5.25.3'; export const PUBLIC_VERSION = '5'; From f49856449dbabb0815066485b23bf404c9c789ff Mon Sep 17 00:00:00 2001 From: Scott Wu Date: Fri, 28 Mar 2025 23:53:10 +0800 Subject: [PATCH 086/145] docs: add a reference to the official hash router (#15611) --- documentation/docs/07-misc/99-faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md index 7e25cdab55..ed5c6277c0 100644 --- a/documentation/docs/07-misc/99-faq.md +++ b/documentation/docs/07-misc/99-faq.md @@ -96,7 +96,7 @@ However, you can use any router library. A lot of people use [page.js](https://g If you prefer a declarative HTML approach, there's the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) library and a fork of it called [svelte-navigator](https://github.com/mefechoel/svelte-navigator) containing some additional functionality. -If you need hash-based routing on the client side, check out [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router) or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/). +If you need hash-based routing on the client side, check out the [hash option](https://svelte.dev/docs/kit/configuration#router) in SvelteKit, [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router), or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/). [Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR. From 04257925d22d8ecef37f50330ac258c7f97dca0c Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 29 Mar 2025 01:42:18 -0700 Subject: [PATCH 087/145] docs: clarify what you can build with SvelteKit (#15461) * docs: clarify what you can build with SvelteKit * try relative URLs * Update documentation/docs/01-introduction/02-getting-started.md Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- documentation/docs/01-introduction/02-getting-started.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/documentation/docs/01-introduction/02-getting-started.md b/documentation/docs/01-introduction/02-getting-started.md index e035e6d6df..c7351729ff 100644 --- a/documentation/docs/01-introduction/02-getting-started.md +++ b/documentation/docs/01-introduction/02-getting-started.md @@ -2,7 +2,7 @@ title: Getting started --- -We recommend using [SvelteKit](../kit), the official application framework from the Svelte team powered by [Vite](https://vite.dev/): +We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with: ```bash npx sv create myapp @@ -15,7 +15,9 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S ## Alternatives to SvelteKit -You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well. +You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well. + +>[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps). There are also plugins for [Rollup](https://github.com/sveltejs/rollup-plugin-svelte), [Webpack](https://github.com/sveltejs/svelte-loader) [and a few others](https://sveltesociety.dev/packages?category=build-plugins), but we recommend Vite. From 6f8068637c6f18649d17687c588b06381318d578 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:48:42 +0200 Subject: [PATCH 088/145] fix: support TS type assertions (#15642) fixes #15565 --- .changeset/lovely-windows-hang.md | 5 +++++ .../src/compiler/phases/1-parse/remove_typescript_nodes.js | 3 +++ .../tests/runtime-runes/samples/typescript/main.svelte | 1 + 3 files changed, 9 insertions(+) create mode 100644 .changeset/lovely-windows-hang.md diff --git a/.changeset/lovely-windows-hang.md b/.changeset/lovely-windows-hang.md new file mode 100644 index 0000000000..406e6c4961 --- /dev/null +++ b/.changeset/lovely-windows-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: support TS type assertions diff --git a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js index 09eb0bfa68..37dc0e17a1 100644 --- a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js +++ b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js @@ -94,6 +94,9 @@ const visitors = { TSTypeAliasDeclaration() { return b.empty; }, + TSTypeAssertion(node, context) { + return context.visit(node.expression); + }, TSEnumDeclaration(node) { e.typescript_invalid_feature(node, 'enums'); }, diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte index e2942b21f3..d2a9da5439 100644 --- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte @@ -45,6 +45,7 @@ export type { Hello }; const TypedFoo = Foo; + const typeAssertion = true; `, + html: ``, compileOptions: { dev: true @@ -34,8 +34,8 @@ export default test({ btn?.click(); }); - assert.htmlEqual(target.innerHTML, ``); + assert.htmlEqual(target.innerHTML, ``); - assert.deepEqual(warnings, []); + assert.deepEqual(warnings, [], 'expected getContext to have widened ownership'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte new file mode 100644 index 0000000000..2dd7cab141 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte @@ -0,0 +1,9 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/sub.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/sub.svelte rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/sub.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js index c07b9ce129..66f1726a2a 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js @@ -1,41 +1,24 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; -/** @type {typeof console.warn} */ -let warn; - -/** @type {any[]} */ -let warnings = []; - export default test({ - html: ``, - compileOptions: { dev: true }, - before_test: () => { - warn = console.warn; - - console.warn = (...args) => { - warnings.push(...args); - }; - }, + test({ assert, target, warnings }) { + const [btn1, btn2] = target.querySelectorAll('button'); - after_test: () => { - console.warn = warn; - warnings = []; - }, + flushSync(() => { + btn1.click(); + }); - test({ assert, target }) { - const btn = target.querySelector('button'); + assert.deepEqual(warnings.length, 0); flushSync(() => { - btn?.click(); + btn2.click(); }); - assert.htmlEqual(target.innerHTML, ``); - - assert.deepEqual(warnings, []); + assert.deepEqual(warnings.length, 1); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte index ad450a937e..0be7e434e4 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte @@ -1,9 +1,8 @@ - - + diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js index 3e7a68cf97..2906b9bce5 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js @@ -1 +1,14 @@ -export let global = $state({}); +export function create_my_state() { + const my_state = $state({ + a: 0 + }); + + function inc() { + my_state.a++; + } + + return { + my_state, + inc + }; +} diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/sub.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/sub.svelte rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/sub.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/Child.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/Child.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js index 96b18d1854..ab7327ab8b 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js @@ -1,41 +1,24 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; -/** @type {typeof console.warn} */ -let warn; - -/** @type {any[]} */ -let warnings = []; - export default test({ - html: ``, - compileOptions: { dev: true }, - before_test: () => { - warn = console.warn; - - console.warn = (...args) => { - warnings.push(...args); - }; - }, + async test({ assert, target, warnings }) { + const [btn1, btn2] = target.querySelectorAll('button'); - after_test: () => { - console.warn = warn; - warnings = []; - }, + flushSync(() => { + btn1.click(); + }); - test({ assert, target }) { - const btn = target.querySelector('button'); + assert.deepEqual(warnings.length, 0); flushSync(() => { - btn?.click(); + btn2.click(); }); - assert.htmlEqual(target.innerHTML, ``); - - assert.deepEqual(warnings, [], 'expected getContext to have widened ownership'); + assert.deepEqual(warnings.length, 1); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte index 2dd7cab141..8e8343790b 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte @@ -1,9 +1,13 @@ - + + diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js deleted file mode 100644 index aeb3740dfe..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js +++ /dev/null @@ -1,37 +0,0 @@ -import { flushSync } 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 = []; - }, - - test({ assert, target }) { - const btn = target.querySelector('button'); - - flushSync(() => { - btn?.click(); - }); - - assert.deepEqual(warnings, []); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte deleted file mode 100644 index 2d40c13949..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js deleted file mode 100644 index 4079059171..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js +++ /dev/null @@ -1,13 +0,0 @@ -class Global { - state = $state({}); - - add_a(a) { - this.state.a = a; - } - - increment_a_b() { - this.state.a.b++; - } -} - -export const global = new Global(); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte deleted file mode 100644 index 044904aa18..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js deleted file mode 100644 index 66f1726a2a..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js +++ /dev/null @@ -1,24 +0,0 @@ -import { flushSync } from 'svelte'; -import { test } from '../../test'; - -export default test({ - compileOptions: { - dev: true - }, - - test({ assert, target, warnings }) { - const [btn1, btn2] = target.querySelectorAll('button'); - - flushSync(() => { - btn1.click(); - }); - - assert.deepEqual(warnings.length, 0); - - flushSync(() => { - btn2.click(); - }); - - assert.deepEqual(warnings.length, 1); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte deleted file mode 100644 index 0be7e434e4..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js deleted file mode 100644 index 2906b9bce5..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js +++ /dev/null @@ -1,14 +0,0 @@ -export function create_my_state() { - const my_state = $state({ - a: 0 - }); - - function inc() { - my_state.a++; - } - - return { - my_state, - inc - }; -} diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte deleted file mode 100644 index aa31fd7606..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js deleted file mode 100644 index cc9ea715f0..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js +++ /dev/null @@ -1,37 +0,0 @@ -import { flushSync } 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'); - - flushSync(() => { - btn?.click(); - }); - - assert.deepEqual(warnings.length, 0); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte deleted file mode 100644 index 92d7dbd2db..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js deleted file mode 100644 index ab7327ab8b..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js +++ /dev/null @@ -1,24 +0,0 @@ -import { flushSync } from 'svelte'; -import { test } from '../../test'; - -export default test({ - compileOptions: { - dev: true - }, - - async test({ assert, target, warnings }) { - const [btn1, btn2] = target.querySelectorAll('button'); - - flushSync(() => { - btn1.click(); - }); - - assert.deepEqual(warnings.length, 0); - - flushSync(() => { - btn2.click(); - }); - - assert.deepEqual(warnings.length, 1); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte deleted file mode 100644 index 8e8343790b..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte deleted file mode 100644 index ffe6ef75c4..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte deleted file mode 100644 index 5f1c7461f6..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js deleted file mode 100644 index 6881c2faf6..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js +++ /dev/null @@ -1,3 +0,0 @@ -export let global = $state({ - object: { count: -1 } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js index 87474a05cc..39fa80c55a 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js @@ -8,6 +8,6 @@ export default test({ }, warnings: [ - 'Intermediate.svelte passed a value to Counter.svelte with `bind:`, but the value is owned by main.svelte. Consider creating a binding between main.svelte and Intermediate.svelte' + 'Intermediate.svelte passed property `object` to Counter.svelte with `bind:`, but its parent component main.svelte did not declare `object` as a binding. Consider creating a binding between main.svelte and Intermediate.svelte (e.g. `bind:object={...}` instead of `object={...}`)' ] }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js index 66e5184380..7b8cc676d5 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js @@ -33,7 +33,7 @@ export default test({ assert.htmlEqual(target.innerHTML, ``); assert.deepEqual(warnings, [ - 'Counter.svelte mutated a value owned by main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead' + 'Mutating unbound props (`notshared`, at Counter.svelte:10:23) is strongly discouraged. Consider using `bind:notshared={...}` in main.svelte (or using a callback) instead' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js index e766a946d0..bd2ecc28b6 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js @@ -1,7 +1,6 @@ import { flushSync } from 'svelte'; import { ok, test } from '../../test'; -// Tests that proxies widen ownership correctly even if not directly connected to each other export default test({ compileOptions: { dev: true diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte deleted file mode 100644 index d6da559fb1..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -

Binding

- - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte deleted file mode 100644 index b935f0a472..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -

Context

- - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js deleted file mode 100644 index d6d12d01cd..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js +++ /dev/null @@ -1,34 +0,0 @@ -import { flushSync } from 'svelte'; -import { test } from '../../test'; - -// Tests that ownership is widened with $derived (on class or on its own) that contains $state -export default test({ - compileOptions: { - dev: true - }, - - test({ assert, target, warnings }) { - const [root, counter_context1, counter_context2, counter_binding1, counter_binding2] = - target.querySelectorAll('button'); - - counter_context1.click(); - counter_context2.click(); - counter_binding1.click(); - counter_binding2.click(); - flushSync(); - - assert.equal(warnings.length, 0); - - root.click(); - flushSync(); - counter_context1.click(); - counter_context2.click(); - counter_binding1.click(); - counter_binding2.click(); - flushSync(); - - assert.equal(warnings.length, 0); - }, - - warnings: [] -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte deleted file mode 100644 index aaade26e16..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - -

Parent

- - - - From 7694818f9cf70cf802058c2a49571aaa69956d8a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:09:22 -0400 Subject: [PATCH 109/145] Version Packages (#15705) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clever-news-enjoy.md | 5 ----- .changeset/sweet-ants-care.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/clever-news-enjoy.md delete mode 100644 .changeset/sweet-ants-care.md diff --git a/.changeset/clever-news-enjoy.md b/.changeset/clever-news-enjoy.md deleted file mode 100644 index 2ff3dcbe56..0000000000 --- a/.changeset/clever-news-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow `$.state` and `$.derived` to be treeshaken diff --git a/.changeset/sweet-ants-care.md b/.changeset/sweet-ants-care.md deleted file mode 100644 index b4805626ab..0000000000 --- a/.changeset/sweet-ants-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: rework binding ownership validation diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 361de202b1..eb76e9a9e9 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.25.9 + +### Patch Changes + +- fix: allow `$.state` and `$.derived` to be treeshaken ([#15702](https://github.com/sveltejs/svelte/pull/15702)) + +- fix: rework binding ownership validation ([#15678](https://github.com/sveltejs/svelte/pull/15678)) + ## 5.25.8 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8a225a798d..3fb843b3a2 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.25.8", + "version": "5.25.9", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2c8140e365..13a69857a7 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.25.8'; +export const VERSION = '5.25.9'; export const PUBLIC_VERSION = '5'; From 708f541ad8ed2e9426cedb03e0bc1ad5d84cb787 Mon Sep 17 00:00:00 2001 From: 7nik Date: Wed, 9 Apr 2025 00:39:11 +0300 Subject: [PATCH 110/145] fix: better scope `:global()` with nesting selector `&` (#15671) Co-authored-by: 7nik --- .changeset/stupid-vans-draw.md | 5 +++++ .../phases/2-analyze/css/css-prune.js | 20 +++++++++++++++---- .../samples/global-with-nesting/expected.css | 7 ++++++- .../samples/global-with-nesting/input.svelte | 7 ++++++- 4 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 .changeset/stupid-vans-draw.md diff --git a/.changeset/stupid-vans-draw.md b/.changeset/stupid-vans-draw.md new file mode 100644 index 0000000000..24892f1e8f --- /dev/null +++ b/.changeset/stupid-vans-draw.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better scope `:global()` with nesting selector `&` diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 0646c6341a..fbe6ca1cd3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -1,6 +1,11 @@ /** @import * as Compiler from '#compiler' */ import { walk } from 'zimmerframe'; -import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js'; +import { + get_parent_rules, + get_possible_values, + is_outer_global, + is_unscoped_pseudo_class +} from './utils.js'; import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; @@ -286,20 +291,26 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi * a global selector * @param {Compiler.AST.CSS.RelativeSelector} selector * @param {Compiler.AST.CSS.Rule} rule + * @returns {boolean} */ function is_global(selector, rule) { if (selector.metadata.is_global || selector.metadata.is_global_like) { return true; } + let explicitly_global = false; + for (const s of selector.selectors) { /** @type {Compiler.AST.CSS.SelectorList | null} */ let selector_list = null; + let can_be_global = false; let owner = rule; if (s.type === 'PseudoClassSelector') { if ((s.name === 'is' || s.name === 'where') && s.args) { selector_list = s.args; + } else { + can_be_global = is_unscoped_pseudo_class(s); } } @@ -308,18 +319,19 @@ function is_global(selector, rule) { selector_list = owner.prelude; } - const has_global_selectors = selector_list?.children.some((complex_selector) => { + const has_global_selectors = !!selector_list?.children.some((complex_selector) => { return complex_selector.children.every((relative_selector) => is_global(relative_selector, owner) ); }); + explicitly_global ||= has_global_selectors; - if (!has_global_selectors) { + if (!has_global_selectors && !can_be_global) { return false; } } - return true; + return explicitly_global || selector.selectors.length === 0; } const regex_backslash_and_following_character = /\\(.)/g; diff --git a/packages/svelte/tests/css/samples/global-with-nesting/expected.css b/packages/svelte/tests/css/samples/global-with-nesting/expected.css index dcb8a0e481..1863c57d85 100644 --- a/packages/svelte/tests/css/samples/global-with-nesting/expected.css +++ b/packages/svelte/tests/css/samples/global-with-nesting/expected.css @@ -1,5 +1,10 @@ div.svelte-xyz { &.class{ - color: red; + color: green; + } + } + * { + &:hover .class.svelte-xyz { + color: green; } } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte index 0c73ed7a78..2c1d2b5ebd 100644 --- a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte +++ b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte @@ -1,7 +1,12 @@ From 966ccfbe7451b7c00fc63ee43ea65bd7f286b5cf Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 8 Apr 2025 23:41:53 +0200 Subject: [PATCH 111/145] fix: set deriveds as `CLEAN` if they are assigned to (#15592) * fix: set deriveds as `CLEAN` if they are assigned to * chore: remove only * chore: remove unnecessary typecast * fix: set unowned as `MAYBE_DIRTY` instead of `CLEAN` * fix: visit the derived function when to update the dependencies even when it's reassigned * fix: use `execute_derived` instead of `update_reaction` * fix: execute deriveds eagerly when they are set if DIRTY --- .changeset/nervous-kids-shake.md | 5 +++ .../internal/client/reactivity/deriveds.js | 2 +- .../src/internal/client/reactivity/sources.js | 12 +++++-- .../svelte/src/internal/client/runtime.js | 2 +- packages/svelte/tests/signals/test.ts | 32 +++++++++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 .changeset/nervous-kids-shake.md diff --git a/.changeset/nervous-kids-shake.md b/.changeset/nervous-kids-shake.md new file mode 100644 index 0000000000..3fc6429797 --- /dev/null +++ b/.changeset/nervous-kids-shake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: set deriveds as `CLEAN` if they are assigned to diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 86171c2b2d..c9a8f7674a 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -131,7 +131,7 @@ function get_derived_parent_effect(derived) { * @param {Derived} derived * @returns {T} */ -function execute_derived(derived) { +export function execute_derived(derived) { var value; var prev_active_effect = active_effect; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 2361762519..cae49c1832 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -28,14 +28,14 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT, - EFFECT_IS_UPDATING + ROOT_EFFECT } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; import { proxy } from '../proxy.js'; +import { execute_derived } from './deriveds.js'; export let inspect_effects = new Set(); export const old_values = new Map(); @@ -172,6 +172,14 @@ export function internal_set(source, value) { } } + if ((source.f & DERIVED) !== 0) { + // if we are assigning to a dirty derived we set it to clean/maybe dirty but we also eagerly execute it to track the dependencies + if ((source.f & DIRTY) !== 0) { + execute_derived(/** @type {Derived} */ (source)); + } + set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY); + } + mark_reactions(source, DIRTY); // It's possible that the current reaction might not have up-to-date dependencies diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c1c91f3551..a7662be617 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; -import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js'; +import { destroy_derived_effects, execute_derived, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { FILENAME } from '../../constants.js'; import { tracing_mode_flag } from '../flags/index.js'; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 3977caae36..3a427e9392 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1080,6 +1080,38 @@ describe('signals', () => { }; }); + test("deriveds set after they are DIRTY doesn't get updated on get", () => { + return () => { + const a = state(0); + const b = derived(() => $.get(a)); + + set(b, 1); + assert.equal($.get(b), 1); + + set(a, 2); + assert.equal($.get(b), 2); + set(b, 3); + + assert.equal($.get(b), 3); + }; + }); + + test("unowned deriveds set after they are DIRTY doesn't get updated on get", () => { + return () => { + const a = state(0); + const b = derived(() => $.get(a)); + const c = derived(() => $.get(b)); + + set(b, 1); + assert.equal($.get(c), 1); + + set(a, 2); + + assert.equal($.get(b), 2); + assert.equal($.get(c), 2); + }; + }); + test('deriveds containing effects work correctly when used with untrack', () => { return () => { let a = render_effect(() => {}); From e9a16c4b4209d104e0bef7cf28df51f830de1254 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Apr 2025 18:53:18 -0400 Subject: [PATCH 112/145] chore: revert corepack override (#15197) * Revert "try this (#15196)" This reverts commit f878736f3825e1832fa7344306358e877e20bd7f. * upgrade node * upgrade pnpm --------- Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- .github/workflows/pkg.pr.new.yml | 5 +---- package.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 90d219faae..b2b521dc6f 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -11,13 +11,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: install corepack - run: npm i -g corepack@0.31.0 - - run: corepack enable - uses: actions/setup-node@v4 with: - node-version: 18.x + node-version: 22.x cache: pnpm - name: Install dependencies diff --git a/package.json b/package.json index ad69bfc9ca..70e85438f0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "type": "module", "license": "MIT", - "packageManager": "pnpm@9.4.0", + "packageManager": "pnpm@10.4.0", "engines": { "pnpm": ">=9.0.0" }, From 0ca1f4a37ec008092fc1798e374c6108308addef Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Tue, 8 Apr 2025 21:26:54 -0400 Subject: [PATCH 113/145] docs: raise importance of global vs local transitions (#15479) * Doc: Raise importance of global vs local transitions * switch order --------- Co-authored-by: Rich Harris --- documentation/docs/03-template-syntax/13-transition.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/docs/03-template-syntax/13-transition.md b/documentation/docs/03-template-syntax/13-transition.md index 51c11e8b34..c51175c272 100644 --- a/documentation/docs/03-template-syntax/13-transition.md +++ b/documentation/docs/03-template-syntax/13-transition.md @@ -22,10 +22,6 @@ The `transition:` directive indicates a _bidirectional_ transition, which means {/if} ``` -## Built-in transitions - -A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module. - ## Local vs global Transitions are local by default. Local transitions only play when the block they belong to is created or destroyed, _not_ when parent blocks are created or destroyed. @@ -40,6 +36,10 @@ Transitions are local by default. Local transitions only play when the block the {/if} ``` +## Built-in transitions + +A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module. + ## Transition parameters Transitions can have parameters. From 0ff3d7452092511a55a63a7639a8e8798fea9fed Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:40:05 -0700 Subject: [PATCH 114/145] docs: update `$effect` examples (#15463) * docs: update effect examples * revert * Update documentation/docs/02-runes/04-$effect.md * update example * revert * update effect root example --------- Co-authored-by: Rich Harris --- documentation/docs/02-runes/04-$effect.md | 55 ++++++++++++++--------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index ae1a2146c9..46ea9b81e9 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -25,7 +25,7 @@ You can create an effect with the `$effect` rune ([demo](/playground/untitled#H4 }); - + ``` When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes. @@ -135,19 +135,33 @@ An effect only reruns when the object it reads changes, not when a property insi An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code. -For instance, if `a` is `true` in the code snippet below, the code inside the `if` block will run and `b` will be evaluated. As such, changes to either `a` or `b` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE3VQzWrDMAx-FdUU4kBp71li6EPstOxge0ox8-QQK2PD-N1nLy2F0Z2Evj9_chKkP1B04pnYscc3cRCT8xhF95IEf8-Vq0DBr8rzPB_jJ3qumNERH-E2ECNxiRF9tIubWY00lgcYNAywj6wZJS8rtk83wjwgCrXHaULLUrYwKEgVGrnkx-Dx6MNFNstK5OjSbFGbwE0gdXuT_zGYrjmAuco515Hr1p_uXak3K3MgCGS9s-9D2grU-judlQYXIencnzad-tdR79qZrMyvw9wd5Z8Yv1h09dz8mn8AkM7Pfo0BAAA=). +For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. As such, changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA). -Conversely, if `a` is `false`, `b` will not be evaluated, and the effect will _only_ re-run when `a` changes. +Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes. ```ts -let a = false; -let b = false; +// @filename: ambient.d.ts +declare module 'canvas-confetti' { + interface ConfettiOptions { + colors: string[]; + } + + function confetti(opts?: ConfettiOptions): void; + export default confetti; +} + +// @filename: index.js // ---cut--- -$effect(() => { - console.log('running'); +import confetti from 'canvas-confetti'; - if (a) { - console.log('b:', b); +let condition = $state(true); +let color = $state('#ff3e00'); + +$effect(() => { + if (condition) { + confetti({ colors: [color] }); + } else { + confetti(); } }); ``` @@ -211,20 +225,19 @@ It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svel The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase. -```svelte - +// later... +destroy(); ``` ## When not to use `$effect` From 93110a32469779794d2c022a7605e958ce75cb34 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 9 Apr 2025 06:30:53 -0400 Subject: [PATCH 115/145] docs: explain restriction on exporting reassigned state (#15713) --- .../01-introduction/04-svelte-js-files.md | 2 +- documentation/docs/02-runes/02-$state.md | 80 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/documentation/docs/01-introduction/04-svelte-js-files.md b/documentation/docs/01-introduction/04-svelte-js-files.md index 0e05484299..1d3e3dd61a 100644 --- a/documentation/docs/01-introduction/04-svelte-js-files.md +++ b/documentation/docs/01-introduction/04-svelte-js-files.md @@ -4,7 +4,7 @@ title: .svelte.js and .svelte.ts files Besides `.svelte` files, Svelte also operates on `.svelte.js` and `.svelte.ts` files. -These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app. +These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app (though note that you [cannot export reassigned state]($state#Passing-state-across-modules)). > [!LEGACY] > This is a concept that didn't exist prior to Svelte 5 diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 49e17cd08f..16630a977b 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -250,3 +250,83 @@ console.log(total.value); // 7 ``` ...though if you find yourself writing code like that, consider using [classes](#Classes) instead. + +## Passing state across modules + +You can declare state in `.svelte.js` and `.svelte.ts` files, but you can only _export_ that state if it's not directly reassigned. In other words you can't do this: + +```js +/// file: state.svelte.js +export let count = $state(0); + +export function increment() { + count += 1; +} +``` + +That's because every reference to `count` is transformed by the Svelte compiler — the code above is roughly equivalent to this: + +```js +/// file: state.svelte.js (compiler output) +// @filename: index.ts +interface Signal { + value: T; +} + +interface Svelte { + state(value?: T): Signal; + get(source: Signal): T; + set(source: Signal, value: T): void; +} +declare const $: Svelte; +// ---cut--- +export let count = $.state(0); + +export function increment() { + $.set(count, $.get(count) + 1); +} +``` + +> [!NOTE] You can see the code Svelte generates by clicking the 'JS Output' tab in the [playground](/playground). + +Since the compiler only operates on one file at a time, if another file imports `count` Svelte doesn't know that it needs to wrap each reference in `$.get` and `$.set`: + +```js +// @filename: state.svelte.js +export let count = 0; + +// @filename: index.js +// ---cut--- +import { count } from './state.svelte.js'; + +console.log(typeof count); // 'object', not 'number' +``` + +This leaves you with two options for sharing state between modules — either don't reassign it... + +```js +// This is allowed — since we're updating +// `counter.count` rather than `counter`, +// Svelte doesn't wrap it in `$.state` +export const counter = $state({ + count: 0 +}); + +export function increment() { + counter.count += 1; +} +``` + +...or don't directly export it: + +```js +let count = $state(0); + +export function getCount() { + return count; +} + +export function increment() { + count += 1; +} +``` From c23f15134e85c17e85ad326fcb50bbc4a1cdffa4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 9 Apr 2025 12:51:21 -0400 Subject: [PATCH 116/145] chore: remove stack-based module boundaries (#15711) --- .../98-reference/.generated/client-errors.md | 2 +- .../svelte/messages/client-errors/errors.md | 2 +- .../3-transform/client/transform-client.js | 3 - .../svelte/src/internal/client/dev/legacy.js | 5 +- .../src/internal/client/dev/ownership.js | 97 ------------------- packages/svelte/src/internal/client/errors.js | 7 +- packages/svelte/src/internal/client/index.js | 2 +- 7 files changed, 7 insertions(+), 111 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index fd9419176d..32348bb781 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -21,7 +21,7 @@ A component is attempting to bind to a non-bindable property `%key%` belonging t ### component_api_changed ``` -%parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5 +Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5 ``` See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information. diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index ca06122cb5..c4e68f8fee 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -12,7 +12,7 @@ ## component_api_changed -> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5 +> Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5 See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information. diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 38fcee8d6f..098b3ecae8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -536,9 +536,6 @@ export function client_component(analysis, options) { b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename)) ) ); - - body.unshift(b.stmt(b.call(b.id('$.mark_module_start')))); - body.push(b.stmt(b.call(b.id('$.mark_module_end'), b.id(analysis.name)))); } if (!analysis.runes) { diff --git a/packages/svelte/src/internal/client/dev/legacy.js b/packages/svelte/src/internal/client/dev/legacy.js index 138213c551..02428dc824 100644 --- a/packages/svelte/src/internal/client/dev/legacy.js +++ b/packages/svelte/src/internal/client/dev/legacy.js @@ -1,7 +1,6 @@ import * as e from '../errors.js'; import { component_context } from '../context.js'; import { FILENAME } from '../../../constants.js'; -import { get_component } from './ownership.js'; /** @param {Function & { [FILENAME]: string }} target */ export function check_target(target) { @@ -15,9 +14,7 @@ export function legacy_api() { /** @param {string} method */ function error(method) { - // @ts-expect-error - const parent = get_component()?.[FILENAME] ?? 'Something'; - e.component_api_changed(parent, method, component[FILENAME]); + e.component_api_changed(method, component[FILENAME]); } return { diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index 6c40a744df..e28a40dd77 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -7,103 +7,6 @@ import { component_context } from '../context.js'; import * as w from '../warnings.js'; import { sanitize_location } from '../../../utils.js'; -/** @type {Record>} */ -const boundaries = {}; - -const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/; -const firefox_pattern = /@(.+):(\d+):(\d+)$/; - -function get_stack() { - const stack = new Error().stack; - if (!stack) return null; - - const entries = []; - - for (const line of stack.split('\n')) { - let match = chrome_pattern.exec(line) ?? firefox_pattern.exec(line); - - if (match) { - entries.push({ - file: match[1], - line: +match[2], - column: +match[3] - }); - } - } - - return entries; -} - -/** - * Determines which `.svelte` component is responsible for a given state change - * @returns {Function | null} - */ -export function get_component() { - // first 4 lines are svelte internals; adjust this number if we change the internal call stack - const stack = get_stack()?.slice(4); - if (!stack) return null; - - for (let i = 0; i < stack.length; i++) { - const entry = stack[i]; - const modules = boundaries[entry.file]; - if (!modules) { - // If the first entry is not a component, that means the modification very likely happened - // within a .svelte.js file, possibly triggered by a component. Since these files are not part - // of the bondaries/component context heuristic, we need to bail in this case, else we would - // have false positives when the .svelte.ts file provides a state creator function, encapsulating - // the state and its mutations, and is being called from a component other than the one who - // called the state creator function. - if (i === 0) return null; - continue; - } - - for (const module of modules) { - if (module.end == null) { - return null; - } - if (module.start.line < entry.line && module.end.line > entry.line) { - return module.component; - } - } - } - - return null; -} - -/** - * 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 - */ -export function mark_module_start() { - const start = get_stack()?.[2]; - - if (start) { - (boundaries[start.file] ??= []).push({ - start, - // @ts-expect-error - end: null, - // @ts-expect-error we add the component at the end, since HMR will overwrite the function - component: null - }); - } -} - -/** - * @param {Function} component - */ -export function mark_module_end(component) { - const end = get_stack()?.[2]; - - if (end) { - const boundaries_file = boundaries[end.file]; - const boundary = boundaries_file[boundaries_file.length - 1]; - - boundary.end = end; - boundary.component = component; - } -} - /** * Sets up a validator that * - traverses the path of a prop to find out if it is allowed to be mutated diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 8a5b5033a7..429dd99da9 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -54,15 +54,14 @@ export function bind_not_bindable(key, component, name) { } /** - * %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5 - * @param {string} parent + * Calling `%method%` on a component instance (of %component%) is no longer valid in Svelte 5 * @param {string} method * @param {string} component * @returns {never} */ -export function component_api_changed(parent, method, component) { +export function component_api_changed(method, component) { if (DEV) { - const error = new Error(`component_api_changed\n${parent} called \`${method}\` on an instance of ${component}, which is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`); + const error = new Error(`component_api_changed\nCalling \`${method}\` on a component instance (of ${component}) is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`); error.name = 'Svelte error'; throw error; diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 7eed8a744a..a865419c5f 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -4,7 +4,7 @@ export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { cleanup_styles } from './dev/css.js'; export { add_locations } from './dev/elements.js'; export { hmr } from './dev/hmr.js'; -export { mark_module_start, mark_module_end, create_ownership_validator } from './dev/ownership.js'; +export { create_ownership_validator } from './dev/ownership.js'; export { check_target, legacy_api } from './dev/legacy.js'; export { trace } from './dev/tracing.js'; export { inspect } from './dev/inspect.js'; From 475b5dbe83732fd031fa4f97aac712550385c700 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:09:51 -0400 Subject: [PATCH 117/145] Version Packages (#15712) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/nervous-kids-shake.md | 5 ----- .changeset/stupid-vans-draw.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/nervous-kids-shake.md delete mode 100644 .changeset/stupid-vans-draw.md diff --git a/.changeset/nervous-kids-shake.md b/.changeset/nervous-kids-shake.md deleted file mode 100644 index 3fc6429797..0000000000 --- a/.changeset/nervous-kids-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: set deriveds as `CLEAN` if they are assigned to diff --git a/.changeset/stupid-vans-draw.md b/.changeset/stupid-vans-draw.md deleted file mode 100644 index 24892f1e8f..0000000000 --- a/.changeset/stupid-vans-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: better scope `:global()` with nesting selector `&` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index eb76e9a9e9..6f999f381e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.25.10 + +### Patch Changes + +- fix: set deriveds as `CLEAN` if they are assigned to ([#15592](https://github.com/sveltejs/svelte/pull/15592)) + +- fix: better scope `:global()` with nesting selector `&` ([#15671](https://github.com/sveltejs/svelte/pull/15671)) + ## 5.25.9 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 3fb843b3a2..b9f434e688 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.25.9", + "version": "5.25.10", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 13a69857a7..2ea9890df9 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.25.9'; +export const VERSION = '5.25.10'; export const PUBLIC_VERSION = '5'; From 6c97a78049ccc706076e8b962d482c7a1fa27650 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 10 Apr 2025 13:29:40 +0200 Subject: [PATCH 118/145] fix: prevent ownership warnings if the fallback of a bindable is used (#15720) * fix: prevent ownership warnings if the fallback of a bindable is used * fix: filter out symbol from own keys * fix: don't create sources for `BINDABLE_FALLBACK_SYMBOL` * fix: use strategy suggested by actually competent person aka @dummdidumm * chore: rename function --- .changeset/wise-turkeys-yell.md | 5 ++++ .../src/internal/client/dev/ownership.js | 12 ++++++---- .../src/internal/client/reactivity/props.js | 2 +- .../Child.svelte | 5 ++++ .../Parent.svelte | 7 ++++++ .../_config.js | 11 +++++++++ .../main.svelte | 5 ++++ .../Parent.svelte | 8 +++++++ .../_config.js | 23 +++++++++++++++++++ .../main.svelte | 5 ++++ 10 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 .changeset/wise-turkeys-yell.md create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte diff --git a/.changeset/wise-turkeys-yell.md b/.changeset/wise-turkeys-yell.md new file mode 100644 index 0000000000..cd5e103de3 --- /dev/null +++ b/.changeset/wise-turkeys-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent ownership warnings if the fallback of a bindable is used diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index e28a40dd77..108c7adf92 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -27,7 +27,7 @@ export function create_ownership_validator(props) { */ mutation: (prop, path, result, line, column) => { const name = path[0]; - if (is_bound(props, name) || !parent) { + if (is_bound_or_unset(props, name) || !parent) { return result; } @@ -52,7 +52,7 @@ export function create_ownership_validator(props) { * @param {() => any} value */ binding: (key, child_component, value) => { - if (!is_bound(props, key) && parent && value()?.[STATE_SYMBOL]) { + if (!is_bound_or_unset(props, key) && parent && value()?.[STATE_SYMBOL]) { w.ownership_invalid_binding( component[FILENAME], key, @@ -68,9 +68,13 @@ export function create_ownership_validator(props) { * @param {Record} props * @param {string} prop_name */ -function is_bound(props, prop_name) { +function is_bound_or_unset(props, prop_name) { // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})` // or `createClassComponent(Component, props)` const is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props; - return !!get_descriptor(props, prop_name)?.set || (is_entry_props && prop_name in props); + return ( + !!get_descriptor(props, prop_name)?.set || + (is_entry_props && prop_name in props) || + !(prop_name in props) + ); } diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index bd85b14df0..341d7c768a 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -7,7 +7,7 @@ import { PROPS_IS_RUNES, PROPS_IS_UPDATED } from '../../../constants.js'; -import { get_descriptor, is_function } from '../../shared/utils.js'; +import { define_property, get_descriptor, is_function } from '../../shared/utils.js'; import { mutable_source, set, source, update } from './sources.js'; import { derived, derived_safe_equal } from './deriveds.js'; import { get, captured_signals, untrack } from '../runtime.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte new file mode 100644 index 0000000000..78b82caed9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Child.svelte @@ -0,0 +1,5 @@ + + +{test} diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte new file mode 100644 index 0000000000..7bfb17aa64 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/Parent.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js new file mode 100644 index 0000000000..e93067eb9d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client'], + compileOptions: { + dev: true + }, + async test({ warnings, assert }) { + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte new file mode 100644 index 0000000000..282afb1771 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-bindable-fallback/main.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte new file mode 100644 index 0000000000..7d6b248da7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/Parent.svelte @@ -0,0 +1,8 @@ + + + + + +{test} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js new file mode 100644 index 0000000000..9b4e3479ea --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + compileOptions: { + dev: true + }, + async test({ warnings, assert, target }) { + const [btn, btn2] = target.querySelectorAll('button'); + flushSync(() => { + btn2.click(); + }); + assert.deepEqual(warnings, []); + flushSync(() => { + btn.click(); + }); + flushSync(() => { + btn2.click(); + }); + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte new file mode 100644 index 0000000000..282afb1771 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-bindable-fallback/main.svelte @@ -0,0 +1,5 @@ + + + From 6d195f0350c5e46da0012525f92654c62257402e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Apr 2025 08:19:07 -0400 Subject: [PATCH 119/145] fix: handle hydration mismatches in await blocks (#15708) * failing test for #15704 * handle hydration mismatches in await blocks * DRY out * changeset * update test --- .changeset/wild-carrots-eat.md | 5 ++++ .../3-transform/server/visitors/AwaitBlock.js | 10 ++----- packages/svelte/src/constants.js | 1 + .../src/internal/client/dom/blocks/await.js | 29 +++++++++++++++++-- packages/svelte/src/internal/server/index.js | 7 +++-- .../await-hydrate-maybe-promise/_config.js | 23 +++++++++++++++ .../await-hydrate-maybe-promise/main.svelte | 25 ++++++++++++++++ .../_expected/server/index.svelte.js | 6 ++-- 8 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 .changeset/wild-carrots-eat.md create mode 100644 packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte diff --git a/.changeset/wild-carrots-eat.md b/.changeset/wild-carrots-eat.md new file mode 100644 index 0000000000..23b55f945c --- /dev/null +++ b/.changeset/wild-carrots-eat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle hydration mismatches in await blocks diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js index 8fc82b8905..2aa534d257 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '../../../../utils/builders.js'; -import { empty_comment } from './shared/utils.js'; +import { block_close } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -10,10 +10,10 @@ import { empty_comment } from './shared/utils.js'; */ export function AwaitBlock(node, context) { context.state.template.push( - empty_comment, b.stmt( b.call( '$.await', + b.id('$$payload'), /** @type {Expression} */ (context.visit(node.expression)), b.thunk( node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([]) @@ -21,13 +21,9 @@ export function AwaitBlock(node, context) { b.arrow( node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [], node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([]) - ), - b.arrow( - node.error ? [/** @type {Pattern} */ (context.visit(node.error))] : [], - node.catch ? /** @type {BlockStatement} */ (context.visit(node.catch)) : b.block([]) ) ) ), - empty_comment + block_close ); } diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 8861e440fc..6ea407d448 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -22,6 +22,7 @@ export const HYDRATION_START = '['; /** used to indicate that an `{:else}...` block was rendered */ export const HYDRATION_START_ELSE = '[!'; export const HYDRATION_END = ']'; +export const HYDRATION_AWAIT_THEN = '!'; export const HYDRATION_ERROR = {}; export const ELEMENT_IS_NAMESPACED = 1; diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 2e3d229779..99bdc0000c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -4,9 +4,16 @@ import { is_promise } from '../../../shared/utils.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { internal_set, mutable_source, source } from '../../reactivity/sources.js'; import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js'; -import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; +import { + hydrate_next, + hydrate_node, + hydrating, + remove_nodes, + set_hydrate_node, + set_hydrating +} from '../hydration.js'; import { queue_micro_task } from '../task.js'; -import { UNINITIALIZED } from '../../../../constants.js'; +import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { component_context, is_runes, @@ -113,6 +120,19 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { var effect = block(() => { if (input === (input = get_input())) return; + /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ + // @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight + let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE); + + if (mismatch) { + // Hydration mismatch: remove everything inside the anchor and start fresh + anchor = remove_nodes(); + + set_hydrate_node(anchor); + set_hydrating(false); + mismatch = true; + } + if (is_promise(input)) { var promise = input; @@ -155,6 +175,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { update(THEN, false); } + if (mismatch) { + // continue in hydration mode + set_hydrating(true); + } + // Set the input to something else, in order to disable the promise callbacks return () => (input = UNINITIALIZED); }); diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index bf36a595d8..ff34c07132 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -13,7 +13,7 @@ import { import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; -import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; +import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js'; import { validate_store } from '../shared/validate.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { reset_elements } from './dev.js'; @@ -474,18 +474,21 @@ export function bind_props(props_parent, props_now) { /** * @template V + * @param {Payload} payload * @param {Promise} promise * @param {null | (() => void)} pending_fn * @param {(value: V) => void} then_fn * @returns {void} */ -function await_block(promise, pending_fn, then_fn) { +function await_block(payload, promise, pending_fn, then_fn) { if (is_promise(promise)) { + payload.out += BLOCK_OPEN; promise.then(null, noop); if (pending_fn !== null) { pending_fn(); } } else if (then_fn !== null) { + payload.out += BLOCK_OPEN_ELSE; then_fn(promise); } } diff --git a/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js new file mode 100644 index 0000000000..f81b41d41a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + ssrHtml: '

42


loading...

', + html: '

loading...


42

', + + props: { + browser: true + }, + + server_props: { + browser: false + }, + + async test({ assert, target }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + await Promise.resolve(); + assert.htmlEqual(target.innerHTML, '

42


42

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte new file mode 100644 index 0000000000..d8d0cd4027 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/await-hydrate-maybe-promise/main.svelte @@ -0,0 +1,25 @@ + + + + +{#await a} + {#if true}

loading...

{/if} +{:then a} +

{a}

+{/await} + +
+ +{#await b} + {#if true}

loading...

{/if} +{:then b} +

{b}

+{/await} diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js index 012789a550..4b6e32d58e 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js @@ -8,7 +8,7 @@ export default function Await_block_scope($$payload) { counter.count += 1; } - $$payload.out += ` `; - $.await(promise, () => {}, (counter) => {}, () => {}); - $$payload.out += ` ${$.escape(counter.count)}`; + $$payload.out += ` `; + $.await($$payload, promise, () => {}, (counter) => {}); + $$payload.out += ` ${$.escape(counter.count)}`; } \ No newline at end of file From c2c83b67f3470fa27afe998e079a48de47b7d9c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:39:50 -0400 Subject: [PATCH 120/145] Version Packages (#15726) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/wild-carrots-eat.md | 5 ----- .changeset/wise-turkeys-yell.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/wild-carrots-eat.md delete mode 100644 .changeset/wise-turkeys-yell.md diff --git a/.changeset/wild-carrots-eat.md b/.changeset/wild-carrots-eat.md deleted file mode 100644 index 23b55f945c..0000000000 --- a/.changeset/wild-carrots-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle hydration mismatches in await blocks diff --git a/.changeset/wise-turkeys-yell.md b/.changeset/wise-turkeys-yell.md deleted file mode 100644 index cd5e103de3..0000000000 --- a/.changeset/wise-turkeys-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent ownership warnings if the fallback of a bindable is used diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6f999f381e..3066c19bce 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.25.11 + +### Patch Changes + +- fix: handle hydration mismatches in await blocks ([#15708](https://github.com/sveltejs/svelte/pull/15708)) + +- fix: prevent ownership warnings if the fallback of a bindable is used ([#15720](https://github.com/sveltejs/svelte/pull/15720)) + ## 5.25.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b9f434e688..187d3aaa2c 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.25.10", + "version": "5.25.11", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2ea9890df9..cc5f8f775c 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.25.10'; +export const VERSION = '5.25.11'; export const PUBLIC_VERSION = '5'; From 9cafdd89d0e8f2fb0ce02e5b5112498218d4f2b5 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 10 Apr 2025 22:18:38 +0100 Subject: [PATCH 121/145] fix: improve internal_set versioning mechanic (#15724) --- .changeset/pretty-planes-visit.md | 5 +++++ .../src/internal/client/reactivity/sources.js | 3 ++- .../samples/writable-derived-2/_config.js | 7 +++++++ .../samples/writable-derived-2/main.svelte | 9 +++++++++ .../samples/writable-derived-2/util.svelte.js | 17 +++++++++++++++++ 5 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .changeset/pretty-planes-visit.md create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js diff --git a/.changeset/pretty-planes-visit.md b/.changeset/pretty-planes-visit.md new file mode 100644 index 0000000000..d2ee0cae2e --- /dev/null +++ b/.changeset/pretty-planes-visit.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve internal_set versioning mechanic diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index cae49c1832..27e8fd824d 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -162,7 +162,6 @@ export function internal_set(source, value) { } source.v = value; - source.wv = increment_write_version(); if (DEV && tracing_mode_flag) { source.updated = get_stack('UpdatedAt'); @@ -180,6 +179,8 @@ export function internal_set(source, value) { set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY); } + source.wv = increment_write_version(); + mark_reactions(source, DIRTY); // It's possible that the current reaction might not have up-to-date dependencies diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/_config.js new file mode 100644 index 0000000000..fde3e7b1ea --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + html: `true true`, + + test({ assert, target, window }) {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte new file mode 100644 index 0000000000..741aa69125 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/main.svelte @@ -0,0 +1,9 @@ + + +{expect1} {expect2} diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js new file mode 100644 index 0000000000..8e862753ab --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-2/util.svelte.js @@ -0,0 +1,17 @@ +export const createAppState = (options) => { + const source = $derived(options.source()); + let value = $derived(source); + + return { + get value() { + return value; + }, + onChange(nextValue) { + value = nextValue; + } + }; +}; + +const result = createAppState({ source: () => 'wrong' }); +result.onChange('right'); +export const expect2 = result.value === 'right'; From 73acf6e7f48957d39d9e261083d652e89bbc94c3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Apr 2025 17:26:25 -0400 Subject: [PATCH 122/145] nicer sandbox output (#15730) --- playgrounds/sandbox/run.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 8fa2c2a2da..3a62286b24 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -56,7 +56,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { }); write( - `${cwd}/output/${file}.json`, + `${cwd}/output/ast/${file}.json`, JSON.stringify( ast, (key, value) => (typeof value === 'bigint' ? ['BigInt', value.toString()] : value), @@ -66,7 +66,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { try { const migrated = migrate(source); - write(`${cwd}/output/${file}.migrated.svelte`, migrated.code); + write(`${cwd}/output/migrated/${file}`, migrated.code); } catch (e) { console.warn(`Error migrating ${file}`, e); } From 0d233e58cf4472cb922118ad76509500273f0d60 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 11 Apr 2025 00:17:22 +0200 Subject: [PATCH 123/145] fix: don't transform reassigned state in labeled statement in `$derived` (#15725) * fix: don't transform reassigned state in labeled statement in `$derived` * fix type so optional chaining is unnecessary * drive-by tidy up * drive-by tidy up --------- Co-authored-by: Rich Harris --- .changeset/weak-doors-yell.md | 5 ++ packages/svelte/src/compiler/migrate/index.js | 72 +++++++++---------- .../input.svelte | 6 ++ .../output.svelte | 10 +++ 4 files changed, 57 insertions(+), 36 deletions(-) create mode 100644 .changeset/weak-doors-yell.md create mode 100644 packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte create mode 100644 packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte diff --git a/.changeset/weak-doors-yell.md b/.changeset/weak-doors-yell.md new file mode 100644 index 0000000000..1b21783435 --- /dev/null +++ b/.changeset/weak-doors-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't transform reassigned state in labeled statement in `$derived` diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 9d79d88b23..523389a25a 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -944,54 +944,53 @@ const instance_script = { node.body.type === 'ExpressionStatement' && node.body.expression.type === 'AssignmentExpression' ) { - const ids = extract_identifiers(node.body.expression.left); - const [, expression_ids] = extract_all_identifiers_from_expression( - node.body.expression.right - ); - const bindings = ids.map((id) => state.scope.get(id.name)); - const reassigned_bindings = bindings.filter((b) => b?.reassigned); + const { left, right } = node.body.expression; - if ( - node.body.expression.right.type !== 'Literal' && - !bindings.some((b) => b?.kind === 'store_sub') && - node.body.expression.left.type !== 'MemberExpression' - ) { - let { start, end } = /** @type {{ start: number, end: number }} */ ( - node.body.expression.right - ); + const ids = extract_identifiers(left); + const [, expression_ids] = extract_all_identifiers_from_expression(right); + const bindings = ids.map((id) => /** @type {Binding} */ (state.scope.get(id.name))); - check_rune_binding('derived'); + if (bindings.every((b) => b.kind === 'legacy_reactive')) { + if ( + right.type !== 'Literal' && + bindings.every((b) => b.kind !== 'store_sub') && + left.type !== 'MemberExpression' + ) { + let { start, end } = /** @type {{ start: number, end: number }} */ (right); - // $derived - state.str.update( - /** @type {number} */ (node.start), - /** @type {number} */ (node.body.expression.start), - 'let ' - ); + check_rune_binding('derived'); - if (node.body.expression.right.type === 'SequenceExpression') { - while (state.str.original[start] !== '(') start -= 1; - while (state.str.original[end - 1] !== ')') end += 1; - } + // $derived + state.str.update( + /** @type {number} */ (node.start), + /** @type {number} */ (node.body.expression.start), + 'let ' + ); + + if (right.type === 'SequenceExpression') { + while (state.str.original[start] !== '(') start -= 1; + while (state.str.original[end - 1] !== ')') end += 1; + } + + state.str.prependRight(start, `$derived(`); - state.str.prependRight(start, `$derived(`); + // in a case like `$: ({ a } = b())`, there's already a trailing parenthesis. + // otherwise, we need to add one + if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') { + state.str.appendLeft(end, `)`); + } - // in a case like `$: ({ a } = b())`, there's already a trailing parenthesis. - // otherwise, we need to add one - if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') { - state.str.appendLeft(end, `)`); + return; } - return; - } else { - for (const binding of reassigned_bindings) { - if (binding && (ids.includes(binding.node) || expression_ids.length === 0)) { + for (const binding of bindings) { + if (binding.reassigned && (ids.includes(binding.node) || expression_ids.length === 0)) { check_rune_binding('state'); const init = binding.kind === 'state' ? ' = $state()' : expression_ids.length === 0 - ? ` = $state(${state.str.original.substring(/** @type {number} */ (node.body.expression.right.start), node.body.expression.right.end)})` + ? ` = $state(${state.str.original.substring(/** @type {number} */ (right.start), right.end)})` : ''; // implicitly-declared variable which we need to make explicit state.str.prependLeft( @@ -1000,7 +999,8 @@ const instance_script = { ); } } - if (expression_ids.length === 0 && !bindings.some((b) => b?.kind === 'store_sub')) { + + if (expression_ids.length === 0 && bindings.every((b) => b.kind !== 'store_sub')) { state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end)); return; } diff --git a/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte new file mode 100644 index 0000000000..0b5c13d889 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/input.svelte @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte new file mode 100644 index 0000000000..c2b36a6e30 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/labeled-statement-reassign-state/output.svelte @@ -0,0 +1,10 @@ + \ No newline at end of file From 0020e597e2137cdc8d6c913885978f0ccb966e3f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 18:39:23 -0400 Subject: [PATCH 124/145] Version Packages (#15729) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/pretty-planes-visit.md | 5 ----- .changeset/weak-doors-yell.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/pretty-planes-visit.md delete mode 100644 .changeset/weak-doors-yell.md diff --git a/.changeset/pretty-planes-visit.md b/.changeset/pretty-planes-visit.md deleted file mode 100644 index d2ee0cae2e..0000000000 --- a/.changeset/pretty-planes-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: improve internal_set versioning mechanic diff --git a/.changeset/weak-doors-yell.md b/.changeset/weak-doors-yell.md deleted file mode 100644 index 1b21783435..0000000000 --- a/.changeset/weak-doors-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't transform reassigned state in labeled statement in `$derived` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 3066c19bce..f27a6640bd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.25.12 + +### Patch Changes + +- fix: improve internal_set versioning mechanic ([#15724](https://github.com/sveltejs/svelte/pull/15724)) + +- fix: don't transform reassigned state in labeled statement in `$derived` ([#15725](https://github.com/sveltejs/svelte/pull/15725)) + ## 5.25.11 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 187d3aaa2c..67f6186fc7 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.25.11", + "version": "5.25.12", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index cc5f8f775c..c554e9018c 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.25.11'; +export const VERSION = '5.25.12'; export const PUBLIC_VERSION = '5'; From ec1d85c89e345529b9108b770c68cf89b808f3b0 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:50:05 -0700 Subject: [PATCH 125/145] fix: add snippet argument validation in dev (#15521) * init * fix * make `Payload` a class * doh * lint * tweak changeset * fix * only export things that should be available on $ * tweak message * fix --------- Co-authored-by: Rich Harris --- .changeset/bright-jeans-compare.md | 5 ++ .../98-reference/.generated/shared-errors.md | 6 ++ .../svelte/messages/shared-errors/errors.md | 4 ++ .../client/visitors/SnippetBlock.js | 17 ++++- .../server/visitors/SnippetBlock.js | 5 +- .../src/internal/client/dev/validation.js | 15 +++++ packages/svelte/src/internal/client/index.js | 1 + .../src/internal/server/blocks/snippet.js | 2 +- packages/svelte/src/internal/server/dev.js | 13 +++- packages/svelte/src/internal/server/index.js | 57 ++--------------- .../svelte/src/internal/server/payload.js | 64 +++++++++++++++++++ .../svelte/src/internal/server/types.d.ts | 13 ---- packages/svelte/src/internal/shared/errors.js | 15 +++++ 13 files changed, 147 insertions(+), 70 deletions(-) create mode 100644 .changeset/bright-jeans-compare.md create mode 100644 packages/svelte/src/internal/client/dev/validation.js create mode 100644 packages/svelte/src/internal/server/payload.js diff --git a/.changeset/bright-jeans-compare.md b/.changeset/bright-jeans-compare.md new file mode 100644 index 0000000000..eaec658e03 --- /dev/null +++ b/.changeset/bright-jeans-compare.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add snippet argument validation in dev diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 0102aafcbc..4c81d7b894 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -30,6 +30,12 @@ This error would be thrown in a setup like this: Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error. +### invalid_snippet_arguments + +``` +A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` +``` + ### lifecycle_outside_component ``` diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 8b4c61303a..20f3d193d9 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -26,6 +26,10 @@ This error would be thrown in a setup like this: Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error. +## invalid_snippet_arguments + +> A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` + ## lifecycle_outside_component > `%name%(...)` can only be used during component initialisation diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 7a0d6981b5..7eb043aa5d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */ +/** @import { AssignmentPattern, BlockStatement, Expression, Identifier, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -12,7 +12,7 @@ import { get_value } from './shared/declarations.js'; */ export function SnippetBlock(node, context) { // TODO hoist where possible - /** @type {Pattern[]} */ + /** @type {(Identifier | AssignmentPattern)[]} */ const args = [b.id('$$anchor')]; /** @type {BlockStatement} */ @@ -66,7 +66,18 @@ export function SnippetBlock(node, context) { } } } - + if (dev) { + declarations.unshift( + b.stmt( + b.call( + '$.validate_snippet_args', + .../** @type {Identifier[]} */ ( + args.map((arg) => (arg?.type === 'Identifier' ? arg : arg?.left)) + ) + ) + ) + ); + } body = b.block([ ...declarations, .../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js index eb83917927..cae3e7d79c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js @@ -1,6 +1,7 @@ /** @import { BlockStatement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ +import { dev } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; /** @@ -13,7 +14,9 @@ export function SnippetBlock(node, context) { [b.id('$$payload'), ...node.parameters], /** @type {BlockStatement} */ (context.visit(node.body)) ); - + if (dev) { + fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload')))); + } // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone fn.___snippet = true; diff --git a/packages/svelte/src/internal/client/dev/validation.js b/packages/svelte/src/internal/client/dev/validation.js new file mode 100644 index 0000000000..e41e4c4628 --- /dev/null +++ b/packages/svelte/src/internal/client/dev/validation.js @@ -0,0 +1,15 @@ +import { invalid_snippet_arguments } from '../../shared/errors.js'; +/** + * @param {Node} anchor + * @param {...(()=>any)[]} args + */ +export function validate_snippet_args(anchor, ...args) { + if (typeof anchor !== 'object' || !(anchor instanceof Node)) { + invalid_snippet_arguments(); + } + for (let arg of args) { + if (typeof arg !== 'function') { + invalid_snippet_arguments(); + } + } +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index a865419c5f..e977bf3b0f 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -8,6 +8,7 @@ export { create_ownership_validator } from './dev/ownership.js'; export { check_target, legacy_api } from './dev/legacy.js'; export { trace } from './dev/tracing.js'; export { inspect } from './dev/inspect.js'; +export { validate_snippet_args } from './dev/validation.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'; diff --git a/packages/svelte/src/internal/server/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js index 3c5e860790..9e96ae3430 100644 --- a/packages/svelte/src/internal/server/blocks/snippet.js +++ b/packages/svelte/src/internal/server/blocks/snippet.js @@ -1,5 +1,5 @@ /** @import { Snippet } from 'svelte' */ -/** @import { Payload } from '#server' */ +/** @import { Payload } from '../payload' */ /** @import { Getters } from '#shared' */ /** diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js index ecf4e67429..34849196b7 100644 --- a/packages/svelte/src/internal/server/dev.js +++ b/packages/svelte/src/internal/server/dev.js @@ -1,10 +1,12 @@ -/** @import { Component, Payload } from '#server' */ +/** @import { Component } from '#server' */ import { FILENAME } from '../../constants.js'; import { is_tag_valid_with_ancestor, is_tag_valid_with_parent } from '../../html-tree-validation.js'; import { current_component } from './context.js'; +import { invalid_snippet_arguments } from '../shared/errors.js'; +import { Payload } from './payload.js'; /** * @typedef {{ @@ -98,3 +100,12 @@ export function push_element(payload, tag, line, column) { export function pop_element() { parent = /** @type {Element} */ (parent).parent; } + +/** + * @param {Payload} payload + */ +export function validate_snippet_args(payload) { + if (typeof payload !== 'object' || !(payload instanceof Payload)) { + invalid_snippet_arguments(); + } +} diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index ff34c07132..d711778a44 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -1,5 +1,5 @@ /** @import { ComponentType, SvelteComponent } from 'svelte' */ -/** @import { Component, Payload, RenderOutput } from '#server' */ +/** @import { Component, RenderOutput } from '#server' */ /** @import { Store } from '#shared' */ export { FILENAME, HMR } from '../../constants.js'; import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; @@ -17,43 +17,13 @@ import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydra import { validate_store } from '../shared/validate.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { reset_elements } from './dev.js'; +import { Payload } from './payload.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter const INVALID_ATTR_NAME_CHAR_REGEX = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; -/** - * @param {Payload} to_copy - * @returns {Payload} - */ -export function copy_payload({ out, css, head, uid }) { - return { - out, - css: new Set(css), - head: { - title: head.title, - out: head.out, - css: new Set(head.css), - uid: head.uid - }, - uid - }; -} - -/** - * Assigns second payload to first - * @param {Payload} p1 - * @param {Payload} p2 - * @returns {void} - */ -export function assign_payload(p1, p2) { - p1.out = p2.out; - p1.css = p2.css; - p1.head = p2.head; - p1.uid = p2.uid; -} - /** * @param {Payload} payload * @param {string} tag @@ -87,16 +57,6 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) */ export let on_destroy = []; -/** - * Creates an ID generator - * @param {string} prefix - * @returns {() => string} - */ -function props_id_generator(prefix) { - let uid = 1; - return () => `${prefix}s${uid++}`; -} - /** * Only available on the server and when compiling with the `server` option. * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. @@ -106,14 +66,7 @@ function props_id_generator(prefix) { * @returns {RenderOutput} */ export function render(component, options = {}) { - const uid = props_id_generator(options.idPrefix ? options.idPrefix + '-' : ''); - /** @type {Payload} */ - const payload = { - out: '', - css: new Set(), - head: { title: '', out: '', css: new Set(), uid }, - uid - }; + const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : ''); const prev_on_destroy = on_destroy; on_destroy = []; @@ -545,7 +498,9 @@ export { html } from './blocks/html.js'; export { push, pop } from './context.js'; -export { push_element, pop_element } from './dev.js'; +export { push_element, pop_element, validate_snippet_args } from './dev.js'; + +export { assign_payload, copy_payload } from './payload.js'; export { snapshot } from '../shared/clone.js'; diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js new file mode 100644 index 0000000000..03bcaf492e --- /dev/null +++ b/packages/svelte/src/internal/server/payload.js @@ -0,0 +1,64 @@ +export class Payload { + /** @type {Set<{ hash: string; code: string }>} */ + css = new Set(); + out = ''; + uid = () => ''; + + head = { + /** @type {Set<{ hash: string; code: string }>} */ + css: new Set(), + title: '', + out: '', + uid: () => '' + }; + + constructor(id_prefix = '') { + this.uid = props_id_generator(id_prefix); + this.head.uid = this.uid; + } +} + +/** + * Used in legacy mode to handle bindings + * @param {Payload} to_copy + * @returns {Payload} + */ +export function copy_payload({ out, css, head, uid }) { + const payload = new Payload(); + + payload.out = out; + payload.css = new Set(css); + payload.uid = uid; + + payload.head = { + title: head.title, + out: head.out, + css: new Set(head.css), + uid: head.uid + }; + + return payload; +} + +/** + * Assigns second payload to first + * @param {Payload} p1 + * @param {Payload} p2 + * @returns {void} + */ +export function assign_payload(p1, p2) { + p1.out = p2.out; + p1.css = p2.css; + p1.head = p2.head; + p1.uid = p2.uid; +} + +/** + * Creates an ID generator + * @param {string} prefix + * @returns {() => string} + */ +function props_id_generator(prefix) { + let uid = 1; + return () => `${prefix}s${uid++}`; +} diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 2fffdbbdf0..6b0fc146c4 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -11,19 +11,6 @@ export interface Component { function?: any; } -export interface Payload { - out: string; - css: Set<{ hash: string; code: string }>; - head: { - title: string; - out: string; - uid: () => string; - css: Set<{ hash: string; code: string }>; - }; - /** Function that generates a unique ID */ - uid: () => string; -} - export interface RenderOutput { /** HTML that goes into the `` */ head: string; diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 26d6822cdb..2e89dc1ad1 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -17,6 +17,21 @@ export function invalid_default_snippet() { } } +/** + * A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` + * @returns {never} + */ +export function invalid_snippet_arguments() { + if (DEV) { + const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. Snippets should only be instantiated via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`); + } +} + /** * `%name%(...)` can only be used during component initialisation * @param {string} name From 01171096cef8652704b92053e57156ae4ed9e5d9 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 11 Apr 2025 04:42:44 +0200 Subject: [PATCH 126/145] feat: add `css.hasGlobal` to `compile` output (#15450) * feat: add `hasUnscopedGlobalCss` to `compile` metadata * chore: rename to `has_unscoped_global` * fix: handle `-global` keyframes * chore: guard the check if the value is already true * update types * add tests * tweak * tweak * regenerate types * Update .changeset/plenty-hotels-mix.md * fix test, add failing test * fix * fix * fix jsdoc * unused * fix * lint * rename * rename * reduce indirection * tidy up * revert * tweak * lint --------- Co-authored-by: Rich Harris --- .changeset/plenty-hotels-mix.md | 5 ++ .../src/compiler/phases/1-parse/read/style.js | 2 + .../phases/2-analyze/css/css-analyze.js | 70 ++++++++++++++----- .../src/compiler/phases/2-analyze/index.js | 3 +- .../compiler/phases/3-transform/css/index.js | 3 +- .../svelte/src/compiler/phases/types.d.ts | 1 + packages/svelte/src/compiler/types/css.d.ts | 5 ++ packages/svelte/src/compiler/types/index.d.ts | 2 + .../css/samples/global-keyframes/_config.js | 5 ++ .../samples/global-local-nested/_config.js | 5 ++ .../samples/global-local-nested/expected.css | 12 ++++ .../samples/global-local-nested/input.svelte | 15 ++++ .../tests/css/samples/global-local/_config.js | 5 ++ .../css/samples/global-local/expected.css | 8 +++ .../css/samples/global-local/input.svelte | 11 +++ .../samples/global-with-nesting/_config.js | 4 +- .../tests/css/samples/global/_config.js | 5 ++ packages/svelte/tests/css/test.ts | 9 +++ packages/svelte/tests/helpers.js | 4 ++ packages/svelte/types/index.d.ts | 2 + 20 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 .changeset/plenty-hotels-mix.md create mode 100644 packages/svelte/tests/css/samples/global-keyframes/_config.js create mode 100644 packages/svelte/tests/css/samples/global-local-nested/_config.js create mode 100644 packages/svelte/tests/css/samples/global-local-nested/expected.css create mode 100644 packages/svelte/tests/css/samples/global-local-nested/input.svelte create mode 100644 packages/svelte/tests/css/samples/global-local/_config.js create mode 100644 packages/svelte/tests/css/samples/global-local/expected.css create mode 100644 packages/svelte/tests/css/samples/global-local/input.svelte create mode 100644 packages/svelte/tests/css/samples/global/_config.js diff --git a/.changeset/plenty-hotels-mix.md b/.changeset/plenty-hotels-mix.md new file mode 100644 index 0000000000..5e7aa834da --- /dev/null +++ b/.changeset/plenty-hotels-mix.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `css.hasGlobal` to `compile` output diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index f8c39f1b1d..56dbe124b7 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -118,6 +118,7 @@ function read_rule(parser) { metadata: { parent_rule: null, has_local_selectors: false, + has_global_selectors: false, is_global_block: false } }; @@ -342,6 +343,7 @@ function read_selector(parser, inside_pseudo_class = false) { children, metadata: { rule: null, + is_global: false, used: false } }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 362ac9dcad..76cb2f56e9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -7,13 +7,15 @@ import { is_keyframes_node } from '../../css.js'; import { is_global, is_unscoped_pseudo_class } from './utils.js'; /** - * @typedef {Visitors< - * AST.CSS.Node, - * { - * keyframes: string[]; - * rule: AST.CSS.Rule | null; - * } - * >} CssVisitors + * @typedef {{ + * keyframes: string[]; + * rule: AST.CSS.Rule | null; + * analysis: ComponentAnalysis; + * }} CssState + */ + +/** + * @typedef {Visitors} CssVisitors */ /** @@ -28,6 +30,15 @@ function is_global_block_selector(simple_selector) { ); } +/** + * @param {AST.SvelteNode[]} path + */ +function is_unscoped(path) { + return path + .filter((node) => node.type === 'Rule') + .every((node) => node.metadata.has_global_selectors); +} + /** * * @param {Array} path @@ -42,6 +53,9 @@ const css_visitors = { if (is_keyframes_node(node)) { if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) { context.state.keyframes.push(node.prelude); + } else if (node.prelude.startsWith('-global-')) { + // we don't check if the block.children.length because the keyframe is still added even if empty + context.state.analysis.css.has_global ||= is_unscoped(context.path); } } @@ -99,10 +113,12 @@ const css_visitors = { node.metadata.rule = context.state.rule; - node.metadata.used ||= node.children.every( + node.metadata.is_global = node.children.every( ({ metadata }) => metadata.is_global || metadata.is_global_like ); + node.metadata.used ||= node.metadata.is_global; + if ( node.metadata.rule?.metadata.parent_rule && node.children[0]?.selectors[0]?.type === 'NestingSelector' @@ -190,6 +206,7 @@ const css_visitors = { if (idx !== -1) { is_global_block = true; + for (let i = idx + 1; i < child.selectors.length; i++) { walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { ComplexSelector(node) { @@ -242,16 +259,26 @@ const css_visitors = { } } - context.next({ - ...context.state, - rule: node - }); + const state = { ...context.state, rule: node }; - node.metadata.has_local_selectors = node.prelude.children.some((selector) => { - return selector.children.some( - ({ metadata }) => !metadata.is_global && !metadata.is_global_like - ); - }); + // visit selector list first, to populate child selector metadata + context.visit(node.prelude, state); + + for (const selector of node.prelude.children) { + node.metadata.has_global_selectors ||= selector.metadata.is_global; + node.metadata.has_local_selectors ||= !selector.metadata.is_global; + } + + // if this rule has a ComplexSelector whose RelativeSelector children are all + // `:global(...)`, and the rule contains declarations (rather than just + // nested rules) then the component as a whole includes global CSS + context.state.analysis.css.has_global ||= + node.metadata.has_global_selectors && + node.block.children.filter((child) => child.type === 'Declaration').length > 0 && + is_unscoped(context.path); + + // visit block list, so parent rule metadata is populated + context.visit(node.block, state); }, NestingSelector(node, context) { const rule = /** @type {AST.CSS.Rule} */ (context.state.rule); @@ -289,5 +316,12 @@ const css_visitors = { * @param {ComponentAnalysis} analysis */ export function analyze_css(stylesheet, analysis) { - walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors); + /** @type {CssState} */ + const css_state = { + keyframes: analysis.css.keyframes, + rule: null, + analysis + }; + + walk(stylesheet, css_state, css_visitors); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index c62fb03e8f..a6eb9565cb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -456,7 +456,8 @@ export function analyze_component(root, source, options) { hash }) : '', - keyframes: [] + keyframes: [], + has_global: false }, source, undefined_exports: new Map(), diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index 5b0dcd5588..dff034f8aa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -59,7 +59,8 @@ export function render_stylesheet(source, analysis, options) { // generateMap takes care of calculating source relative to file source: options.filename, file: options.cssOutputFilename || options.filename - }) + }), + hasGlobal: analysis.css.has_global }; merge_with_preprocessor_map(css, options, css.map.sources[0]); diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index f09b881303..f98cbe1415 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -74,6 +74,7 @@ export interface ComponentAnalysis extends Analysis { ast: AST.CSS.StyleSheet | null; hash: string; keyframes: string[]; + has_global: boolean; }; source: string; undefined_exports: Map; diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 7b2e6ae5f7..154a06ffb1 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -34,6 +34,10 @@ export namespace _CSS { metadata: { parent_rule: null | Rule; has_local_selectors: boolean; + /** + * `true` if the rule contains a ComplexSelector whose RelativeSelectors are all global or global-like + */ + has_global_selectors: boolean; /** * `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped */ @@ -64,6 +68,7 @@ export namespace _CSS { /** @internal */ metadata: { rule: null | Rule; + is_global: boolean; /** True if this selector applies to an element. For global selectors, this is defined in css-analyze, for others in css-prune while scoping */ used: boolean; }; diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index eec41bad9d..616c346ad3 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -18,6 +18,8 @@ export interface CompileResult { code: string; /** A source map */ map: SourceMap; + /** Whether or not the CSS includes global rules */ + hasGlobal: boolean; }; /** * An array of warning objects that were generated during compilation. Each warning has several properties: diff --git a/packages/svelte/tests/css/samples/global-keyframes/_config.js b/packages/svelte/tests/css/samples/global-keyframes/_config.js new file mode 100644 index 0000000000..30953854ad --- /dev/null +++ b/packages/svelte/tests/css/samples/global-keyframes/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + hasGlobal: true +}); diff --git a/packages/svelte/tests/css/samples/global-local-nested/_config.js b/packages/svelte/tests/css/samples/global-local-nested/_config.js new file mode 100644 index 0000000000..5a7796ebac --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local-nested/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + hasGlobal: false +}); diff --git a/packages/svelte/tests/css/samples/global-local-nested/expected.css b/packages/svelte/tests/css/samples/global-local-nested/expected.css new file mode 100644 index 0000000000..8eadf2b948 --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local-nested/expected.css @@ -0,0 +1,12 @@ + + div.svelte-xyz { + .whatever { + color: green; + } + } + + .whatever { + div.svelte-xyz { + color: green; + } + } diff --git a/packages/svelte/tests/css/samples/global-local-nested/input.svelte b/packages/svelte/tests/css/samples/global-local-nested/input.svelte new file mode 100644 index 0000000000..60210be753 --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local-nested/input.svelte @@ -0,0 +1,15 @@ +
{@html whatever}
+ + diff --git a/packages/svelte/tests/css/samples/global-local/_config.js b/packages/svelte/tests/css/samples/global-local/_config.js new file mode 100644 index 0000000000..5a7796ebac --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + hasGlobal: false +}); diff --git a/packages/svelte/tests/css/samples/global-local/expected.css b/packages/svelte/tests/css/samples/global-local/expected.css new file mode 100644 index 0000000000..c4fc74fb1a --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local/expected.css @@ -0,0 +1,8 @@ + + div.svelte-xyz .whatever { + color: green; + } + + .whatever div.svelte-xyz { + color: green; + } diff --git a/packages/svelte/tests/css/samples/global-local/input.svelte b/packages/svelte/tests/css/samples/global-local/input.svelte new file mode 100644 index 0000000000..bff97ab485 --- /dev/null +++ b/packages/svelte/tests/css/samples/global-local/input.svelte @@ -0,0 +1,11 @@ +
{@html whatever}
+ + diff --git a/packages/svelte/tests/css/samples/global-with-nesting/_config.js b/packages/svelte/tests/css/samples/global-with-nesting/_config.js index 292c6c49ac..6cec7c2360 100644 --- a/packages/svelte/tests/css/samples/global-with-nesting/_config.js +++ b/packages/svelte/tests/css/samples/global-with-nesting/_config.js @@ -1,5 +1,7 @@ import { test } from '../../test'; export default test({ - warnings: [] + warnings: [], + + hasGlobal: false }); diff --git a/packages/svelte/tests/css/samples/global/_config.js b/packages/svelte/tests/css/samples/global/_config.js new file mode 100644 index 0000000000..30953854ad --- /dev/null +++ b/packages/svelte/tests/css/samples/global/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + hasGlobal: true +}); diff --git a/packages/svelte/tests/css/test.ts b/packages/svelte/tests/css/test.ts index dd51f52eab..8846b1d986 100644 --- a/packages/svelte/tests/css/test.ts +++ b/packages/svelte/tests/css/test.ts @@ -34,6 +34,7 @@ interface CssTest extends BaseTest { compileOptions?: Partial; warnings?: Warning[]; props?: Record; + hasGlobal?: boolean; } /** @@ -78,6 +79,14 @@ const { test, run } = suite(async (config, cwd) => { // assert_html_equal(actual_ssr, expected.html); } + if (config.hasGlobal !== undefined) { + const metadata = JSON.parse( + fs.readFileSync(`${cwd}/_output/client/input.svelte.css.json`, 'utf-8') + ); + + assert.equal(metadata.hasGlobal, config.hasGlobal); + } + const dom_css = fs.readFileSync(`${cwd}/_output/client/input.svelte.css`, 'utf-8').trim(); const ssr_css = fs.readFileSync(`${cwd}/_output/server/input.svelte.css`, 'utf-8').trim(); diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index 87bcb473e7..f853d5873c 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -146,6 +146,10 @@ export async function compile_directory( if (compiled.css) { write(`${output_dir}/${file}.css`, compiled.css.code); + write( + `${output_dir}/${file}.css.json`, + JSON.stringify({ hasGlobal: compiled.css.hasGlobal }) + ); if (output_map) { write(`${output_dir}/${file}.css.map`, JSON.stringify(compiled.css.map, null, '\t')); } diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c6000fc4b6..6f12daf187 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -753,6 +753,8 @@ declare module 'svelte/compiler' { code: string; /** A source map */ map: SourceMap; + /** Whether or not the CSS includes global rules */ + hasGlobal: boolean; }; /** * An array of warning objects that were generated during compilation. Each warning has several properties: From 02448a9acdf781ea44b696d4a60f387b8856e82c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 23:19:23 -0400 Subject: [PATCH 127/145] Version Packages (#15731) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/bright-jeans-compare.md | 5 ----- .changeset/plenty-hotels-mix.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/bright-jeans-compare.md delete mode 100644 .changeset/plenty-hotels-mix.md diff --git a/.changeset/bright-jeans-compare.md b/.changeset/bright-jeans-compare.md deleted file mode 100644 index eaec658e03..0000000000 --- a/.changeset/bright-jeans-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add snippet argument validation in dev diff --git a/.changeset/plenty-hotels-mix.md b/.changeset/plenty-hotels-mix.md deleted file mode 100644 index 5e7aa834da..0000000000 --- a/.changeset/plenty-hotels-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add `css.hasGlobal` to `compile` output diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f27a6640bd..68a8803663 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.26.0 + +### Minor Changes + +- feat: add `css.hasGlobal` to `compile` output ([#15450](https://github.com/sveltejs/svelte/pull/15450)) + +### Patch Changes + +- fix: add snippet argument validation in dev ([#15521](https://github.com/sveltejs/svelte/pull/15521)) + ## 5.25.12 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 67f6186fc7..8267cb5218 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.25.12", + "version": "5.26.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c554e9018c..ade51aaf17 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.25.12'; +export const VERSION = '5.26.0'; export const PUBLIC_VERSION = '5'; From 95a020acead1a403ae2d4320bea57227e52143ec Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Apr 2025 10:01:51 -0400 Subject: [PATCH 128/145] fix: update `state_referenced_locally` message (#15733) * fix: update state_referenced_locally message * changeset * update message --- .changeset/stale-gorillas-judge.md | 5 ++++ .../.generated/compile-warnings.md | 9 +++--- .../messages/compile-warnings/script.md | 9 +++--- .../phases/2-analyze/visitors/Identifier.js | 30 ++++++++++++++++++- packages/svelte/src/compiler/warnings.js | 8 +++-- .../static-state-reference/input.svelte | 1 + .../static-state-reference/warnings.json | 24 +++++++++++---- 7 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 .changeset/stale-gorillas-judge.md diff --git a/.changeset/stale-gorillas-judge.md b/.changeset/stale-gorillas-judge.md new file mode 100644 index 0000000000..3d91f401dd --- /dev/null +++ b/.changeset/stale-gorillas-judge.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: update `state_referenced_locally` message diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 57396bd7fd..0e94cbadb2 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -823,15 +823,16 @@ See [the migration guide](v5-migration-guide#Snippets-instead-of-slots) for more ### state_referenced_locally ``` -State referenced in its own scope will never update. Did you mean to reference it inside a closure? +This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead? ``` This warning is thrown when the compiler detects the following: + - A reactive variable is declared -- the variable is reassigned -- the variable is referenced inside the same scope it is declared and it is a non-reactive context +- ...and later reassigned... +- ...and referenced in the same scope -In this case, the state reassignment will not be noticed by whatever you passed it to. For example, if you pass the state to a function, that function will not notice the updates: +This 'breaks the link' to the original state declaration. For example, if you pass the state to a function, the function loses access to the state once it is reassigned: ```svelte diff --git a/packages/svelte/messages/compile-warnings/script.md b/packages/svelte/messages/compile-warnings/script.md index 8c32fb7082..6603759156 100644 --- a/packages/svelte/messages/compile-warnings/script.md +++ b/packages/svelte/messages/compile-warnings/script.md @@ -54,14 +54,15 @@ To fix this, wrap your variable declaration with `$state`. ## state_referenced_locally -> State referenced in its own scope will never update. Did you mean to reference it inside a closure? +> This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead? This warning is thrown when the compiler detects the following: + - A reactive variable is declared -- the variable is reassigned -- the variable is referenced inside the same scope it is declared and it is a non-reactive context +- ...and later reassigned... +- ...and referenced in the same scope -In this case, the state reassignment will not be noticed by whatever you passed it to. For example, if you pass the state to a function, that function will not notice the updates: +This 'breaks the link' to the original state declaration. For example, if you pass the state to a function, the function loses access to the state once it is reassigned: ```svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index 79dccd5a7c..dcbe564543 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -7,6 +7,7 @@ import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { is_rune } from '../../../../utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; +import { get_rune } from '../../scope.js'; /** * @param {Identifier} node @@ -111,7 +112,34 @@ export function Identifier(node, context) { (parent.type !== 'AssignmentExpression' || parent.left !== node) && parent.type !== 'UpdateExpression' ) { - w.state_referenced_locally(node); + let type = 'closure'; + + let i = context.path.length; + while (i--) { + const parent = context.path[i]; + + if ( + parent.type === 'ArrowFunctionExpression' || + parent.type === 'FunctionDeclaration' || + parent.type === 'FunctionExpression' + ) { + break; + } + + if ( + parent.type === 'CallExpression' && + parent.arguments.includes(/** @type {any} */ (context.path[i + 1])) + ) { + const rune = get_rune(parent, context.state.scope); + + if (rune === '$state' || rune === '$state.raw') { + type = 'derived'; + break; + } + } + } + + w.state_referenced_locally(node, node.name, type); } if ( diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index a9ea617d3f..e6fc8caba5 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -641,11 +641,13 @@ export function reactive_declaration_module_script_dependency(node) { } /** - * State referenced in its own scope will never update. Did you mean to reference it inside a closure? + * This reference only captures the initial value of `%name%`. Did you mean to reference it inside a %type% instead? * @param {null | NodeLike} node + * @param {string} name + * @param {string} type */ -export function state_referenced_locally(node) { - w(node, 'state_referenced_locally', `State referenced in its own scope will never update. Did you mean to reference it inside a closure?\nhttps://svelte.dev/e/state_referenced_locally`); +export function state_referenced_locally(node, name, type) { + w(node, 'state_referenced_locally', `This reference only captures the initial value of \`${name}\`. Did you mean to reference it inside a ${type} instead?\nhttps://svelte.dev/e/state_referenced_locally`); } /** diff --git a/packages/svelte/tests/validator/samples/static-state-reference/input.svelte b/packages/svelte/tests/validator/samples/static-state-reference/input.svelte index cd0c7b7349..577527ee60 100644 --- a/packages/svelte/tests/validator/samples/static-state-reference/input.svelte +++ b/packages/svelte/tests/validator/samples/static-state-reference/input.svelte @@ -2,6 +2,7 @@ let obj = $state({ a: 0 }); let count = $state(0); let doubled = $derived(count * 2); + let tripled = $state(count * 3); console.log(obj); console.log(count); diff --git a/packages/svelte/tests/validator/samples/static-state-reference/warnings.json b/packages/svelte/tests/validator/samples/static-state-reference/warnings.json index 9ba0941519..a118d5e4a0 100644 --- a/packages/svelte/tests/validator/samples/static-state-reference/warnings.json +++ b/packages/svelte/tests/validator/samples/static-state-reference/warnings.json @@ -1,26 +1,38 @@ [ { "code": "state_referenced_locally", - "message": "State referenced in its own scope will never update. Did you mean to reference it inside a closure?", + "message": "This reference only captures the initial value of `count`. Did you mean to reference it inside a derived instead?", + "start": { + "column": 22, + "line": 5 + }, + "end": { + "column": 27, + "line": 5 + } + }, + { + "code": "state_referenced_locally", + "message": "This reference only captures the initial value of `count`. Did you mean to reference it inside a closure instead?", "start": { "column": 13, - "line": 7 + "line": 8 }, "end": { "column": 18, - "line": 7 + "line": 8 } }, { "code": "state_referenced_locally", - "message": "State referenced in its own scope will never update. Did you mean to reference it inside a closure?", + "message": "This reference only captures the initial value of `doubled`. Did you mean to reference it inside a closure instead?", "start": { "column": 13, - "line": 8 + "line": 9 }, "end": { "column": 20, - "line": 8 + "line": 9 } } ] From 9b2507131ca0134b59cef7da1fdd59603c519f0a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Apr 2025 10:50:11 -0400 Subject: [PATCH 129/145] chore: remove unused documentation markdown files (#15738) --- .../docs/01-introduction/xx-props.md | 139 ----------------- .../xx-reactivity-fundamentals.md | 144 ------------------ .../03-template-syntax/xx-control-flow.md | 111 -------------- .../03-template-syntax/xx-data-fetching.md | 20 --- .../docs/07-misc/xx-reactivity-indepth.md | 6 - 5 files changed, 420 deletions(-) delete mode 100644 documentation/docs/01-introduction/xx-props.md delete mode 100644 documentation/docs/01-introduction/xx-reactivity-fundamentals.md delete mode 100644 documentation/docs/03-template-syntax/xx-control-flow.md delete mode 100644 documentation/docs/03-template-syntax/xx-data-fetching.md delete mode 100644 documentation/docs/07-misc/xx-reactivity-indepth.md diff --git a/documentation/docs/01-introduction/xx-props.md b/documentation/docs/01-introduction/xx-props.md deleted file mode 100644 index cad854d878..0000000000 --- a/documentation/docs/01-introduction/xx-props.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: Public API of a component ---- - -### Public API of a component - -Svelte uses the `$props` rune to declare _properties_ or _props_, which means describing the public interface of the component which becomes accessible to consumers of the component. - -> [!NOTE] `$props` is one of several runes, which are special hints for Svelte's compiler to make things reactive. - -```svelte - -``` - -You can specify a fallback value for a prop. It will be used if the component's consumer doesn't specify the prop on the component when instantiating the component, or if the passed value is `undefined` at some point. - -```svelte - -``` - -To get all properties, use rest syntax: - -```svelte - -``` - -You can use reserved words as prop names. - -```svelte - -``` - -If you're using TypeScript, you can declare the prop types: - -```svelte - -``` - -If you're using JavaScript, you can declare the prop types using JSDoc: - -```svelte - -``` - -If you export a `const`, `class` or `function`, it is readonly from outside the component. - -```svelte - -``` - -Readonly props can be accessed as properties on the element, tied to the component using [`bind:this` syntax](bindings#bind:this). - -### Reactive variables - -To change component state and trigger a re-render, just assign to a locally declared variable that was declared using the `$state` rune. - -Update expressions (`count += 1`) and property assignments (`obj.x = y`) have the same effect. - -```svelte - -``` - -Svelte's ` -``` - -If you'd like to react to changes to a prop, use the `$derived` or `$effect` runes instead. - -```svelte - -``` - -For more information on reactivity, read the documentation around runes. diff --git a/documentation/docs/01-introduction/xx-reactivity-fundamentals.md b/documentation/docs/01-introduction/xx-reactivity-fundamentals.md deleted file mode 100644 index d5e67ada71..0000000000 --- a/documentation/docs/01-introduction/xx-reactivity-fundamentals.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -title: Reactivity fundamentals ---- - -Reactivity is at the heart of interactive UIs. When you click a button, you expect some kind of response. It's your job as a developer to make this happen. It's Svelte's job to make your job as intuitive as possible, by providing a good API to express reactive systems. - -## Runes - -Svelte 5 uses _runes_, a powerful set of primitives for controlling reactivity inside your Svelte components and inside `.svelte.js` and `.svelte.ts` modules. - -Runes are function-like symbols that provide instructions to the Svelte compiler. You don't need to import them from anywhere — when you use Svelte, they're part of the language. - -The following sections introduce the most important runes for declare state, derived state and side effects at a high level. For more details refer to the later sections on [state](state) and [side effects](side-effects). - -## `$state` - -Reactive state is declared with the `$state` rune: - -```svelte - - - -``` - -You can also use `$state` in class fields (whether public or private): - -```js -// @errors: 7006 2554 -class Todo { - done = $state(false); - text = $state(); - - constructor(text) { - this.text = text; - } -} -``` - -> [!LEGACY] -> In Svelte 4, state was implicitly reactive if the variable was declared at the top level -> -> ```svelte -> -> -> -> ``` - -## `$derived` - -Derived state is declared with the `$derived` rune: - -```svelte - - - - -

{count} doubled is {doubled}

-``` - -The expression inside `$derived(...)` should be free of side-effects. Svelte will disallow state changes (e.g. `count++`) inside derived expressions. - -As with `$state`, you can mark class fields as `$derived`. - -> [!LEGACY] -> In Svelte 4, you could use reactive statements for this. -> -> ```svelte -> -> -> -> ->

{count} doubled is {doubled}

-> ``` -> -> This only worked at the top level of a component. - -## `$effect` - -To run _side-effects_ when the component is mounted to the DOM, and when values change, we can use the `$effect` rune ([demo](/playground/untitled#H4sIAAAAAAAAE31T24rbMBD9lUG7kAQ2sbdlX7xOYNk_aB_rQhRpbAsU2UiTW0P-vbrYubSlYGzmzMzROTPymdVKo2PFjzMzfIusYB99z14YnfoQuD1qQh-7bmdFQEonrOppVZmKNBI49QthCc-OOOH0LZ-9jxnR6c7eUpOnuv6KeT5JFdcqbvbcBcgDz1jXKGg6ncFyBedYR6IzLrAZwiN5vtSxaJA-EzadfJEjKw11C6GR22-BLH8B_wxdByWpvUYtqqal2XB6RVkG1CoHB6U1WJzbnYFDiwb3aGEdDa3Bm1oH12sQLTcNPp7r56m_00mHocSG97_zd7ICUXonA5fwKbPbkE2ZtMJGGVkEdctzQi4QzSwr9prnFYNk5hpmqVuqPQjNnfOJoMF22lUsrq_UfIN6lfSVyvQ7grB3X2mjMZYO3XO9w-U5iLx42qg29md3BP_ni5P4gy9ikTBlHxjLzAtPDlyYZmRdjAbGq7HprEQ7p64v4LU_guu0kvAkhBim3nMplWl8FreQD-CW20aZR0wq12t-KqDWeBywhvexKC3memmDwlHAv9q4Vo2ZK8KtK0CgX7u9J8wXbzdKv-nRnfF_2baTqlYoWUF2h5efl9-n0O6koAMAAA==)): - -```svelte - - - -``` - -The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied. - -> [!LEGACY] -> In Svelte 4, you could use reactive statements for this. -> -> ```svelte -> -> -> -> ``` -> -> This only worked at the top level of a component. diff --git a/documentation/docs/03-template-syntax/xx-control-flow.md b/documentation/docs/03-template-syntax/xx-control-flow.md deleted file mode 100644 index b73917997b..0000000000 --- a/documentation/docs/03-template-syntax/xx-control-flow.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Control flow ---- - -- if -- each -- await (or move that into some kind of data loading section?) -- NOT: key (move into transition section, because that's the common use case) - -Svelte augments HTML with control flow blocks to be able to express conditionally rendered content or lists. - -The syntax between these blocks is the same: - -- `{#` denotes the start of a block -- `{:` denotes a different branch part of the block. Depending on the block, there can be multiple of these -- `{/` denotes the end of a block - -## {#if ...} - -## {#each ...} - -```svelte - -{#each expression as name}...{/each} -``` - -```svelte - -{#each expression as name, index}...{/each} -``` - -```svelte - -{#each expression as name (key)}...{/each} -``` - -```svelte - -{#each expression as name, index (key)}...{/each} -``` - -```svelte - -{#each expression as name}...{:else}...{/each} -``` - -Iterating over lists of values can be done with an each block. - -```svelte -

Shopping list

-
    - {#each items as item} -
  • {item.name} x {item.qty}
  • - {/each} -
-``` - -You can use each blocks to iterate over any array or array-like value — that is, any object with a `length` property. - -An each block can also specify an _index_, equivalent to the second argument in an `array.map(...)` callback: - -```svelte -{#each items as item, i} -
  • {i + 1}: {item.name} x {item.qty}
  • -{/each} -``` - -If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. - -```svelte -{#each items as item (item.id)} -
  • {item.name} x {item.qty}
  • -{/each} - - -{#each items as item, i (item.id)} -
  • {i + 1}: {item.name} x {item.qty}
  • -{/each} -``` - -You can freely use destructuring and rest patterns in each blocks. - -```svelte -{#each items as { id, name, qty }, i (id)} -
  • {i + 1}: {name} x {qty}
  • -{/each} - -{#each objects as { id, ...rest }} -
  • {id}
  • -{/each} - -{#each items as [id, ...rest]} -
  • {id}
  • -{/each} -``` - -An each block can also have an `{:else}` clause, which is rendered if the list is empty. - -```svelte -{#each todos as todo} -

    {todo.text}

    -{:else} -

    No tasks today!

    -{/each} -``` - -It is possible to iterate over iterables like `Map` or `Set`. Iterables need to be finite and static (they shouldn't change while being iterated over). Under the hood, they are transformed to an array using `Array.from` before being passed off to rendering. If you're writing performance-sensitive code, try to avoid iterables and use regular arrays as they are more performant. - -## Other block types - -Svelte also provides [`#snippet`](snippets), [`#key`](transitions-and-animations) and [`#await`](data-fetching) blocks. You can find out more about them in their respective sections. diff --git a/documentation/docs/03-template-syntax/xx-data-fetching.md b/documentation/docs/03-template-syntax/xx-data-fetching.md deleted file mode 100644 index 4526d51335..0000000000 --- a/documentation/docs/03-template-syntax/xx-data-fetching.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Data fetching ---- - -Fetching data is a fundamental part of apps interacting with the outside world. Svelte is unopinionated with how you fetch your data. The simplest way would be using the built-in `fetch` method: - -```svelte - -``` - -While this works, it makes working with promises somewhat unergonomic. Svelte alleviates this problem using the `#await` block. - -## {#await ...} - -## SvelteKit loaders - -Fetching inside your components is great for simple use cases, but it's prone to data loading waterfalls and makes code harder to work with because of the promise handling. SvelteKit solves this problem by providing a opinionated data loading story that is coupled to its router. Learn more about it [in the docs](../kit). diff --git a/documentation/docs/07-misc/xx-reactivity-indepth.md b/documentation/docs/07-misc/xx-reactivity-indepth.md deleted file mode 100644 index b40072552f..0000000000 --- a/documentation/docs/07-misc/xx-reactivity-indepth.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Reactivity in depth ---- - -- how to think about Runes ("just JavaScript" with added reactivity, what this means for keeping reactivity alive across boundaries) -- signals From fd57eb362ddf6e9e43936a7d098087f5e4720181 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:16:49 -0400 Subject: [PATCH 130/145] Version Packages (#15737) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/stale-gorillas-judge.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/stale-gorillas-judge.md diff --git a/.changeset/stale-gorillas-judge.md b/.changeset/stale-gorillas-judge.md deleted file mode 100644 index 3d91f401dd..0000000000 --- a/.changeset/stale-gorillas-judge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: update `state_referenced_locally` message diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 68a8803663..8b46efc94c 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.26.1 + +### Patch Changes + +- fix: update `state_referenced_locally` message ([#15733](https://github.com/sveltejs/svelte/pull/15733)) + ## 5.26.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8267cb5218..a06c73429a 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.26.0", + "version": "5.26.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index ade51aaf17..e5cb34ecd5 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.26.0'; +export const VERSION = '5.26.1'; export const PUBLIC_VERSION = '5'; From d38504d073e9a6704660a4b98718b577975ce5d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:56:33 -0400 Subject: [PATCH 131/145] chore(deps-dev): bump vite from 5.4.17 to 5.4.18 (#15743) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.17 to 5.4.18. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.4.18/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.4.18/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 5.4.18 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- playgrounds/sandbox/package.json | 2 +- pnpm-lock.yaml | 86 ++++++++++++++++---------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json index d392349b60..5aee92ab17 100644 --- a/playgrounds/sandbox/package.json +++ b/playgrounds/sandbox/package.json @@ -18,7 +18,7 @@ "polka": "^1.0.0-next.25", "svelte": "workspace:*", "tinyglobby": "^0.2.12", - "vite": "^5.4.17", + "vite": "^5.4.18", "vite-plugin-inspect": "^0.8.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0e66d1681..3518b0e57e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,7 +152,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^4.0.0-next.6 - version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) polka: specifier: ^1.0.0-next.25 version: 1.0.0-next.25 @@ -163,11 +163,11 @@ importers: specifier: ^0.2.12 version: 0.2.12 vite: - specifier: ^5.4.17 - version: 5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + specifier: ^5.4.18 + version: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vite-plugin-inspect: specifier: ^0.8.4 - version: 0.8.4(rollup@4.39.0)(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 0.8.4(rollup@4.39.0)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) packages: @@ -816,8 +816,8 @@ packages: resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.29.0': - resolution: {integrity: sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==} + '@typescript-eslint/scope-manager@8.29.1': + resolution: {integrity: sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/type-utils@8.26.0': @@ -831,8 +831,8 @@ packages: resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.29.0': - resolution: {integrity: sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==} + '@typescript-eslint/types@8.29.1': + resolution: {integrity: sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.26.0': @@ -841,8 +841,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/typescript-estree@8.29.0': - resolution: {integrity: sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==} + '@typescript-eslint/typescript-estree@8.29.1': + resolution: {integrity: sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' @@ -854,8 +854,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.29.0': - resolution: {integrity: sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==} + '@typescript-eslint/utils@8.29.1': + resolution: {integrity: sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -865,8 +865,8 @@ packages: resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.29.0': - resolution: {integrity: sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==} + '@typescript-eslint/visitor-keys@8.29.1': + resolution: {integrity: sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/coverage-v8@2.0.5': @@ -2310,8 +2310,8 @@ packages: terser: optional: true - vite@5.4.17: - resolution: {integrity: sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==} + vite@5.4.18: + resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -3000,25 +3000,25 @@ snapshots: typescript: 5.5.4 typescript-eslint: 8.26.0(eslint@9.9.1)(typescript@5.5.4) - '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.0 svelte: link:packages/svelte - vite: 5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: link:packages/svelte - vite: 5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vitefu: 0.2.5(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vitefu: 0.2.5(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) transitivePeerDependencies: - supports-color @@ -3088,10 +3088,10 @@ snapshots: '@typescript-eslint/types': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/scope-manager@8.29.0': + '@typescript-eslint/scope-manager@8.29.1': dependencies: - '@typescript-eslint/types': 8.29.0 - '@typescript-eslint/visitor-keys': 8.29.0 + '@typescript-eslint/types': 8.29.1 + '@typescript-eslint/visitor-keys': 8.29.1 '@typescript-eslint/type-utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: @@ -3106,7 +3106,7 @@ snapshots: '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/types@8.29.0': {} + '@typescript-eslint/types@8.29.1': {} '@typescript-eslint/typescript-estree@8.26.0(typescript@5.5.4)': dependencies: @@ -3122,10 +3122,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.29.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.29.1(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.29.0 - '@typescript-eslint/visitor-keys': 8.29.0 + '@typescript-eslint/types': 8.29.1 + '@typescript-eslint/visitor-keys': 8.29.1 debug: 4.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -3147,12 +3147,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.29.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/utils@8.29.1(eslint@9.9.1)(typescript@5.5.4)': dependencies: '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1) - '@typescript-eslint/scope-manager': 8.29.0 - '@typescript-eslint/types': 8.29.0 - '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.29.1 + '@typescript-eslint/types': 8.29.1 + '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.5.4) eslint: 9.9.1 typescript: 5.5.4 transitivePeerDependencies: @@ -3163,9 +3163,9 @@ snapshots: '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.0 - '@typescript-eslint/visitor-keys@8.29.0': + '@typescript-eslint/visitor-keys@8.29.1': dependencies: - '@typescript-eslint/types': 8.29.0 + '@typescript-eslint/types': 8.29.1 eslint-visitor-keys: 4.2.0 '@vitest/coverage-v8@2.0.5(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': @@ -3509,7 +3509,7 @@ snapshots: eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4): dependencies: '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1) - '@typescript-eslint/utils': 8.29.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.29.1(eslint@9.9.1)(typescript@5.5.4) enhanced-resolve: 5.18.1 eslint: 9.9.1 eslint-plugin-es-x: 7.8.0(eslint@9.9.1) @@ -4562,7 +4562,7 @@ snapshots: debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 1.1.2 - vite: 5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - '@types/node' - less @@ -4574,7 +4574,7 @@ snapshots: - supports-color - terser - vite-plugin-inspect@0.8.4(rollup@4.39.0)(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-plugin-inspect@0.8.4(rollup@4.39.0)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: '@antfu/utils': 0.7.8 '@rollup/pluginutils': 5.1.0(rollup@4.39.0) @@ -4585,7 +4585,7 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 2.0.4 - vite: 5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - rollup - supports-color @@ -4602,7 +4602,7 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): + vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: esbuild: 0.21.5 postcss: 8.5.3 @@ -4614,9 +4614,9 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vitefu@0.2.5(vite@5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vitefu@0.2.5(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): optionalDependencies: - vite: 5.4.17(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: From a5240ccfe48ecc2f9eebf3de45ffcd60825bc1b0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Apr 2025 18:49:38 -0400 Subject: [PATCH 132/145] use pnpm/action-setup (#15744) --- .github/workflows/pkg.pr.new.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index b2b521dc6f..b1ba217e5a 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -8,10 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - run: corepack enable + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22.x From 3d6da41b1d3aaf80b5e9cb9aa8aaa1f91ffc485b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Apr 2025 22:19:01 -0400 Subject: [PATCH 133/145] chore: remove some unused code (#15747) --- .../svelte/src/internal/client/reactivity/sources.js | 3 +-- packages/svelte/src/internal/client/runtime.js | 9 +-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 27e8fd824d..69a41338c0 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */ +/** @import { Derived, Effect, Source, Value } from '#client' */ import { DEV } from 'esm-env'; import { active_reaction, @@ -12,7 +12,6 @@ import { increment_write_version, update_effect, reaction_sources, - set_reaction_sources, check_dirtiness, untracking, is_destroying_effect, diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a7662be617..2acad3d258 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -94,18 +94,11 @@ export function set_active_effect(effect) { */ export let reaction_sources = null; -/** - * @param {Source[] | null} sources - */ -export function set_reaction_sources(sources) { - reaction_sources = sources; -} - /** @param {Value} value */ export function push_reaction_value(value) { if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { if (reaction_sources === null) { - set_reaction_sources([value]); + reaction_sources = [value]; } else { reaction_sources.push(value); } From 69a427518dc4eeae39ec8be8c55486d0093784df Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 12 Apr 2025 16:39:22 +0200 Subject: [PATCH 134/145] fix: correctly validate `undefined` snippet params with default value (#15750) * fix: correctly validate `undefined` snippet params with default value * use arguments * unused * drive-by --------- Co-authored-by: Rich Harris --- .changeset/angry-mayflies-matter.md | 5 +++ .../client/visitors/SnippetBlock.js | 34 ++++++------------- .../_config.js | 5 +++ .../main.svelte | 5 +++ 4 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 .changeset/angry-mayflies-matter.md create mode 100644 packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte diff --git a/.changeset/angry-mayflies-matter.md b/.changeset/angry-mayflies-matter.md new file mode 100644 index 0000000000..289fbf87b6 --- /dev/null +++ b/.changeset/angry-mayflies-matter.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly validate `undefined` snippet params with default value diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 7eb043aa5d..f28f8c8a59 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -21,6 +21,10 @@ export function SnippetBlock(node, context) { /** @type {Statement[]} */ const declarations = []; + if (dev) { + declarations.push(b.stmt(b.call('$.validate_snippet_args', b.spread(b.id('arguments'))))); + } + const transform = { ...context.state.transform }; const child_state = { ...context.state, transform }; @@ -30,12 +34,7 @@ export function SnippetBlock(node, context) { if (!argument) continue; if (argument.type === 'Identifier') { - args.push({ - type: 'AssignmentPattern', - left: argument, - right: b.id('$.noop') - }); - + args.push(b.assignment_pattern(argument, b.id('$.noop'))); transform[argument.name] = { read: b.call }; continue; @@ -66,29 +65,16 @@ export function SnippetBlock(node, context) { } } } - if (dev) { - declarations.unshift( - b.stmt( - b.call( - '$.validate_snippet_args', - .../** @type {Identifier[]} */ ( - args.map((arg) => (arg?.type === 'Identifier' ? arg : arg?.left)) - ) - ) - ) - ); - } + body = b.block([ ...declarations, .../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body ]); - /** @type {Expression} */ - let snippet = b.arrow(args, body); - - if (dev) { - snippet = b.call('$.wrap_snippet', b.id(context.state.analysis.name), snippet); - } + // in dev we use a FunctionExpression (not arrow function) so we can use `arguments` + let snippet = dev + ? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body)) + : b.arrow(args, body); const declaration = b.const(node.expression, snippet); diff --git a/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js new file mode 100644 index 0000000000..bddb75e677 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

    default

    ` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte new file mode 100644 index 0000000000..3f00eba46b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/validate-undefined-snippet-default-arg/main.svelte @@ -0,0 +1,5 @@ +{#snippet test(param = "default")} +

    {param}

    +{/snippet} + +{@render test()} \ No newline at end of file From 3153384928c444b484fd504268ba87aa580396a1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:42:58 -0400 Subject: [PATCH 135/145] Version Packages (#15751) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/angry-mayflies-matter.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/angry-mayflies-matter.md diff --git a/.changeset/angry-mayflies-matter.md b/.changeset/angry-mayflies-matter.md deleted file mode 100644 index 289fbf87b6..0000000000 --- a/.changeset/angry-mayflies-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly validate `undefined` snippet params with default value diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8b46efc94c..3c876880b8 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.26.2 + +### Patch Changes + +- fix: correctly validate `undefined` snippet params with default value ([#15750](https://github.com/sveltejs/svelte/pull/15750)) + ## 5.26.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a06c73429a..75568f4a77 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.26.1", + "version": "5.26.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e5cb34ecd5..2dc75f7e59 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.26.1'; +export const VERSION = '5.26.2'; export const PUBLIC_VERSION = '5'; From fd0bc2997340bb8164e2b77996e81645315b8c5d Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sun, 13 Apr 2025 12:20:48 +0200 Subject: [PATCH 136/145] fix: correctly validate head snippets on the server (#15755) * fix: correctly validate head snippets on the server * put the logic in copy_payload so it gets treeshaken in most cases --------- Co-authored-by: Rich Harris --- .changeset/dirty-zebras-do.md | 5 +++ packages/svelte/src/internal/server/dev.js | 8 +++-- .../svelte/src/internal/server/payload.js | 34 ++++++++++++------- .../head-payload-validation/_config.js | 11 ++++++ .../head-payload-validation/main.svelte | 7 ++++ 5 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 .changeset/dirty-zebras-do.md create mode 100644 packages/svelte/tests/runtime-runes/samples/head-payload-validation/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte diff --git a/.changeset/dirty-zebras-do.md b/.changeset/dirty-zebras-do.md new file mode 100644 index 0000000000..f0b266b09c --- /dev/null +++ b/.changeset/dirty-zebras-do.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly validate head snippets on the server diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js index 34849196b7..157f22f929 100644 --- a/packages/svelte/src/internal/server/dev.js +++ b/packages/svelte/src/internal/server/dev.js @@ -6,7 +6,7 @@ import { } from '../../html-tree-validation.js'; import { current_component } from './context.js'; import { invalid_snippet_arguments } from '../shared/errors.js'; -import { Payload } from './payload.js'; +import { HeadPayload, Payload } from './payload.js'; /** * @typedef {{ @@ -105,7 +105,11 @@ export function pop_element() { * @param {Payload} payload */ export function validate_snippet_args(payload) { - if (typeof payload !== 'object' || !(payload instanceof Payload)) { + if ( + typeof payload !== 'object' || + // for some reason typescript consider the type of payload as never after the first instanceof + !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload) + ) { invalid_snippet_arguments(); } } diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js index 03bcaf492e..8df5787ba4 100644 --- a/packages/svelte/src/internal/server/payload.js +++ b/packages/svelte/src/internal/server/payload.js @@ -1,16 +1,25 @@ +export class HeadPayload { + /** @type {Set<{ hash: string; code: string }>} */ + css = new Set(); + out = ''; + uid = () => ''; + title = ''; + + constructor(css = new Set(), out = '', title = '', uid = () => '') { + this.css = css; + this.out = out; + this.title = title; + this.uid = uid; + } +} + export class Payload { /** @type {Set<{ hash: string; code: string }>} */ css = new Set(); out = ''; uid = () => ''; - head = { - /** @type {Set<{ hash: string; code: string }>} */ - css: new Set(), - title: '', - out: '', - uid: () => '' - }; + head = new HeadPayload(); constructor(id_prefix = '') { this.uid = props_id_generator(id_prefix); @@ -30,12 +39,11 @@ export function copy_payload({ out, css, head, uid }) { payload.css = new Set(css); payload.uid = uid; - payload.head = { - title: head.title, - out: head.out, - css: new Set(head.css), - uid: head.uid - }; + payload.head = new HeadPayload(); + payload.head.out = head.out; + payload.head.css = new Set(head.css); + payload.head.title = head.title; + payload.head.uid = head.uid; return payload; } diff --git a/packages/svelte/tests/runtime-runes/samples/head-payload-validation/_config.js b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/_config.js new file mode 100644 index 0000000000..7c609205df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + mode: ['server'], + async test({ errors, assert }) { + assert.equal(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte new file mode 100644 index 0000000000..7eb31d3a9e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/head-payload-validation/main.svelte @@ -0,0 +1,7 @@ +{#snippet head()} + Cool +{/snippet} + + + {@render head()} + \ No newline at end of file From de4376235cb355d818ba7aaa17c00a4fe76fe9cd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Apr 2025 07:02:58 -0400 Subject: [PATCH 137/145] docs: add some missing details around string coercion and handling of nullish values (#15739) closes #14716 --- documentation/docs/02-runes/05-$props.md | 4 ++-- documentation/docs/03-template-syntax/01-basic-markup.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md index f300fb239d..222b4831b6 100644 --- a/documentation/docs/02-runes/05-$props.md +++ b/documentation/docs/02-runes/05-$props.md @@ -37,7 +37,7 @@ On the other side, inside `MyComponent.svelte`, we can receive props with the `$ ## Fallback values -Destructuring allows us to declare fallback values, which are used if the parent component does not set a given prop: +Destructuring allows us to declare fallback values, which are used if the parent component does not set a given prop (or the value is `undefined`): ```js let { adjective = 'happy' } = $props(); @@ -219,4 +219,4 @@ This is useful for linking elements via attributes like `for` and `aria-labelled -``` \ No newline at end of file +``` diff --git a/documentation/docs/03-template-syntax/01-basic-markup.md b/documentation/docs/03-template-syntax/01-basic-markup.md index 5e8b4342d3..fe5f8b02aa 100644 --- a/documentation/docs/03-template-syntax/01-basic-markup.md +++ b/documentation/docs/03-template-syntax/01-basic-markup.md @@ -154,6 +154,8 @@ A JavaScript expression can be included as text by surrounding it with curly bra {expression} ``` +Expressions that are `null` or `undefined` will be omitted; all others are [coerced to strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion). + Curly braces can be included in a Svelte template by using their [HTML entity](https://developer.mozilla.org/docs/Glossary/Entity) strings: `{`, `{`, or `{` for `{` and `}`, `}`, or `}` for `}`. If you're using a regular expression (`RegExp`) [literal notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#literal_notation_and_constructor), you'll need to wrap it in parentheses. From bdf033e30f40b7b40792eafe694a02ce84387089 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:56:43 +0200 Subject: [PATCH 138/145] fix: allow self-closing tags within math namespace (#15761) fixes #15757 --- .changeset/wicked-cheetahs-juggle.md | 5 +++++ .../compiler/phases/2-analyze/visitors/RegularElement.js | 3 ++- .../samples/invalid-self-closing-tag/input.svelte | 1 + .../samples/invalid-self-closing-tag/warnings.json | 8 ++++---- 4 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .changeset/wicked-cheetahs-juggle.md diff --git a/.changeset/wicked-cheetahs-juggle.md b/.changeset/wicked-cheetahs-juggle.md new file mode 100644 index 0000000000..58dca62bec --- /dev/null +++ b/.changeset/wicked-cheetahs-juggle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow self-closing tags within math namespace diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js index 03dfaebcb7..d5689e5d55 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js @@ -173,7 +173,8 @@ export function RegularElement(node, context) { if ( context.state.analysis.source[node.end - 2] === '/' && !is_void(node_name) && - !is_svg(node_name) + !is_svg(node_name) && + !is_mathml(node_name) ) { w.element_invalid_self_closing_tag(node, node.name); } diff --git a/packages/svelte/tests/validator/samples/invalid-self-closing-tag/input.svelte b/packages/svelte/tests/validator/samples/invalid-self-closing-tag/input.svelte index 376c9f79bd..07582e8343 100644 --- a/packages/svelte/tests/validator/samples/invalid-self-closing-tag/input.svelte +++ b/packages/svelte/tests/validator/samples/invalid-self-closing-tag/input.svelte @@ -1,6 +1,7 @@ + diff --git a/packages/svelte/tests/validator/samples/invalid-self-closing-tag/warnings.json b/packages/svelte/tests/validator/samples/invalid-self-closing-tag/warnings.json index 40b87ec7c8..3e45bca5ad 100644 --- a/packages/svelte/tests/validator/samples/invalid-self-closing-tag/warnings.json +++ b/packages/svelte/tests/validator/samples/invalid-self-closing-tag/warnings.json @@ -3,11 +3,11 @@ "code": "element_invalid_self_closing_tag", "message": "Self-closing HTML tags for non-void elements are ambiguous — use `
    ` rather than `
    `", "start": { - "line": 8, + "line": 9, "column": 0 }, "end": { - "line": 8, + "line": 9, "column": 7 } }, @@ -15,11 +15,11 @@ "code": "element_invalid_self_closing_tag", "message": "Self-closing HTML tags for non-void elements are ambiguous — use `` rather than ``", "start": { - "line": 9, + "line": 10, "column": 0 }, "end": { - "line": 9, + "line": 10, "column": 12 } } From 26e574f27f6866383044a85da9d6845944bd0fe2 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:57:37 +0200 Subject: [PATCH 139/145] fix: ignore mutation validation for props that are not proxies in more cases (#15759) This fixes an off-by-one error - we did not bail if the top level of the prop wasn't a state already. Fixes #15727 --- .changeset/modern-ducks-reflect.md | 5 +++++ .../src/internal/client/dev/ownership.js | 7 ++++--- .../samples/non-local-mutation-ok/_config.js | 18 ++++++++++++++++ .../non-local-mutation-ok/child.svelte | 8 +++++++ .../samples/non-local-mutation-ok/main.svelte | 21 +++++++++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 .changeset/modern-ducks-reflect.md create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte diff --git a/.changeset/modern-ducks-reflect.md b/.changeset/modern-ducks-reflect.md new file mode 100644 index 0000000000..dfbb9a18cc --- /dev/null +++ b/.changeset/modern-ducks-reflect.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ignore mutation validation for props that are not proxies in more cases diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index 108c7adf92..5a8af6d522 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -31,13 +31,14 @@ export function create_ownership_validator(props) { return result; } - let value = props[name]; + /** @type {any} */ + let value = props; - for (let i = 1; i < path.length - 1; i++) { + for (let i = 0; i < path.length - 1; i++) { + value = value[path[i]]; if (!value?.[STATE_SYMBOL]) { return result; } - value = value[path[i]]; } const location = sanitize_location(`${component[FILENAME]}:${line}:${column}`); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js new file mode 100644 index 0000000000..437385e185 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/_config.js @@ -0,0 +1,18 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + test({ assert, target, warnings }) { + const btn = target.querySelector('button'); + btn?.click(); + flushSync(); + + assert.deepEqual(warnings, []); + }, + + warnings: [] +}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte new file mode 100644 index 0000000000..0243a6c7d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/child.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte new file mode 100644 index 0000000000..8685664ab1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-ok/main.svelte @@ -0,0 +1,21 @@ + + + From db111f61ea51edd655b13a0949dc696bb8688d81 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Apr 2025 09:08:19 -0400 Subject: [PATCH 140/145] docs: headers for snippet prop section (#15745) Closes #14020 Closes #15172 --- documentation/docs/03-template-syntax/06-snippet.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/documentation/docs/03-template-syntax/06-snippet.md b/documentation/docs/03-template-syntax/06-snippet.md index c9951d3f34..ab536c6e5c 100644 --- a/documentation/docs/03-template-syntax/06-snippet.md +++ b/documentation/docs/03-template-syntax/06-snippet.md @@ -112,6 +112,8 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4 ## Passing snippets to components +### Explicit props + Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)): ```svelte @@ -144,6 +146,8 @@ Within the template, snippets are values just like any other. As such, they can Think about it like passing content instead of data to a component. The concept is similar to slots in web components. +### Implicit props + As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)): ```svelte @@ -165,6 +169,8 @@ As an authoring convenience, snippets declared directly _inside_ a component imp ``` +### Implicit `children` snippet + Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)): ```svelte @@ -184,6 +190,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic > [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name +### Optional snippet props + You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set... ```svelte From 7aed6beeaabbcdbaa0a6fab952c6e06a0b385edd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:12:10 -0400 Subject: [PATCH 141/145] Version Packages (#15756) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/dirty-zebras-do.md | 5 ----- .changeset/modern-ducks-reflect.md | 5 ----- .changeset/wicked-cheetahs-juggle.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/dirty-zebras-do.md delete mode 100644 .changeset/modern-ducks-reflect.md delete mode 100644 .changeset/wicked-cheetahs-juggle.md diff --git a/.changeset/dirty-zebras-do.md b/.changeset/dirty-zebras-do.md deleted file mode 100644 index f0b266b09c..0000000000 --- a/.changeset/dirty-zebras-do.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly validate head snippets on the server diff --git a/.changeset/modern-ducks-reflect.md b/.changeset/modern-ducks-reflect.md deleted file mode 100644 index dfbb9a18cc..0000000000 --- a/.changeset/modern-ducks-reflect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ignore mutation validation for props that are not proxies in more cases diff --git a/.changeset/wicked-cheetahs-juggle.md b/.changeset/wicked-cheetahs-juggle.md deleted file mode 100644 index 58dca62bec..0000000000 --- a/.changeset/wicked-cheetahs-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow self-closing tags within math namespace diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 3c876880b8..58f2317796 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.26.3 + +### Patch Changes + +- fix: correctly validate head snippets on the server ([#15755](https://github.com/sveltejs/svelte/pull/15755)) + +- fix: ignore mutation validation for props that are not proxies in more cases ([#15759](https://github.com/sveltejs/svelte/pull/15759)) + +- fix: allow self-closing tags within math namespace ([#15761](https://github.com/sveltejs/svelte/pull/15761)) + ## 5.26.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 75568f4a77..b8654671ec 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.26.2", + "version": "5.26.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2dc75f7e59..1909588146 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.26.2'; +export const VERSION = '5.26.3'; export const PUBLIC_VERSION = '5'; From a051f96ed607751f502dadcd9cbaff1bf880b15f Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:38:02 +0200 Subject: [PATCH 142/145] fix: relax `:global` selector list validation (#15762) We have to allow `:global x, :global y` selector lists because CSS preprocessors might generate that from `:global { x, y {...} }` --- .changeset/green-starfishes-shave.md | 5 ++ .../98-reference/.generated/compile-errors.md | 26 +++++- .../svelte/messages/compile-errors/style.md | 26 +++++- packages/svelte/src/compiler/errors.js | 4 +- .../phases/2-analyze/css/css-analyze.js | 90 +++++++++---------- .../compiler/phases/3-transform/css/index.js | 37 +++++--- .../css-global-block-multiple-1/_config.js | 10 +++ .../css-global-block-multiple-1/main.svelte | 9 ++ .../css-global-block-multiple-2/_config.js | 10 +++ .../css-global-block-multiple-2/main.svelte | 6 ++ .../css-global-block-multiple/_config.js | 9 -- .../css-global-block-multiple/main.svelte | 3 - .../tests/css/samples/global-block/_config.js | 14 +++ .../css/samples/global-block/expected.css | 10 +++ .../css/samples/global-block/input.svelte | 10 +++ 15 files changed, 198 insertions(+), 71 deletions(-) create mode 100644 .changeset/green-starfishes-shave.md create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte diff --git a/.changeset/green-starfishes-shave.md b/.changeset/green-starfishes-shave.md new file mode 100644 index 0000000000..967bba753c --- /dev/null +++ b/.changeset/green-starfishes-shave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: relax `:global` selector list validation diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index a8c39aaf97..6196a85ade 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -235,7 +235,31 @@ A top-level `:global {...}` block can only contain rules, not declarations ### css_global_block_invalid_list ``` -A `:global` selector cannot be part of a selector list with more than one item +A `:global` selector cannot be part of a selector list with entries that don't contain `:global` +``` + +The following CSS is invalid: + +```css +:global, x { + y { + color: red; + } +} +``` + +This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors: + +```css +:global { + y { + color: red; + } +} + +x y { + color: red; +} ``` ### css_global_block_invalid_modifier diff --git a/packages/svelte/messages/compile-errors/style.md b/packages/svelte/messages/compile-errors/style.md index 1e1ab45e8c..f08a2156a3 100644 --- a/packages/svelte/messages/compile-errors/style.md +++ b/packages/svelte/messages/compile-errors/style.md @@ -16,7 +16,31 @@ ## css_global_block_invalid_list -> A `:global` selector cannot be part of a selector list with more than one item +> A `:global` selector cannot be part of a selector list with entries that don't contain `:global` + +The following CSS is invalid: + +```css +:global, x { + y { + color: red; + } +} +``` + +This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors: + +```css +:global { + y { + color: red; + } +} + +x y { + color: red; +} +``` ## css_global_block_invalid_modifier diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 6bf973948b..aa328764e1 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -555,12 +555,12 @@ export function css_global_block_invalid_declaration(node) { } /** - * A `:global` selector cannot be part of a selector list with more than one item + * A `:global` selector cannot be part of a selector list with entries that don't contain `:global` * @param {null | number | NodeLike} node * @returns {never} */ export function css_global_block_invalid_list(node) { - e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with more than one item\nhttps://svelte.dev/e/css_global_block_invalid_list`); + e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with entries that don't contain \`:global\`\nhttps://svelte.dev/e/css_global_block_invalid_list`); } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 76cb2f56e9..2dc3435648 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -193,10 +193,12 @@ const css_visitors = { Rule(node, context) { node.metadata.parent_rule = context.state.rule; - node.metadata.is_global_block = node.prelude.children.some((selector) => { + // We gotta allow :global x, :global y because CSS preprocessors might generate that from :global { x, y {...} } + for (const complex_selector of node.prelude.children) { let is_global_block = false; - for (const child of selector.children) { + for (let selector_idx = 0; selector_idx < complex_selector.children.length; selector_idx++) { + const child = complex_selector.children[selector_idx]; const idx = child.selectors.findIndex(is_global_block_selector); if (is_global_block) { @@ -204,58 +206,56 @@ const css_visitors = { child.metadata.is_global_like = true; } - if (idx !== -1) { - is_global_block = true; + if (idx === 0) { + if ( + child.selectors.length > 1 && + selector_idx === 0 && + node.metadata.parent_rule === null + ) { + e.css_global_block_invalid_modifier_start(child.selectors[1]); + } else { + // `child` starts with `:global` + node.metadata.is_global_block = is_global_block = true; + + for (let i = 1; i < child.selectors.length; i++) { + walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { + ComplexSelector(node) { + node.metadata.used = true; + } + }); + } - for (let i = idx + 1; i < child.selectors.length; i++) { - walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { - ComplexSelector(node) { - node.metadata.used = true; - } - }); - } - } - } + if (child.combinator && child.combinator.name !== ' ') { + e.css_global_block_invalid_combinator(child, child.combinator.name); + } - return is_global_block; - }); + const declaration = node.block.children.find((child) => child.type === 'Declaration'); + const is_lone_global = + complex_selector.children.length === 1 && + complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x` - if (node.metadata.is_global_block) { - if (node.prelude.children.length > 1) { - e.css_global_block_invalid_list(node.prelude); - } + if (is_lone_global && node.prelude.children.length > 1) { + // `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always + // constrained by `x`, which is not what the user intended + e.css_global_block_invalid_list(node.prelude); + } - const complex_selector = node.prelude.children[0]; - const global_selector = complex_selector.children.find((r, selector_idx) => { - const idx = r.selectors.findIndex(is_global_block_selector); - if (idx === 0) { - if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) { - e.css_global_block_invalid_modifier_start(r.selectors[1]); + if ( + declaration && + // :global { color: red; } is invalid, but foo :global { color: red; } is valid + node.prelude.children.length === 1 && + is_lone_global + ) { + e.css_global_block_invalid_declaration(declaration); + } } - return true; } else if (idx !== -1) { - e.css_global_block_invalid_modifier(r.selectors[idx]); + e.css_global_block_invalid_modifier(child.selectors[idx]); } - }); - - if (!global_selector) { - throw new Error('Internal error: global block without :global selector'); - } - - if (global_selector.combinator && global_selector.combinator.name !== ' ') { - e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name); } - const declaration = node.block.children.find((child) => child.type === 'Declaration'); - - if ( - declaration && - // :global { color: red; } is invalid, but foo :global { color: red; } is valid - node.prelude.children.length === 1 && - node.prelude.children[0].children.length === 1 && - node.prelude.children[0].children[0].selectors.length === 1 - ) { - e.css_global_block_invalid_declaration(declaration); + if (node.metadata.is_global_block && !is_global_block) { + e.css_global_block_invalid_list(node.prelude); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index dff034f8aa..9f1142cce9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -170,7 +170,11 @@ const visitors = { if (node.metadata.is_global_block) { const selector = node.prelude.children[0]; - if (selector.children.length === 1 && selector.children[0].selectors.length === 1) { + if ( + node.prelude.children.length === 1 && + selector.children.length === 1 && + selector.children[0].selectors.length === 1 + ) { // `:global {...}` if (state.minify) { state.code.remove(node.start, node.block.start + 1); @@ -194,7 +198,7 @@ const visitors = { SelectorList(node, { state, next, path }) { // Only add comments if we're not inside a complex selector that itself is unused or a global block if ( - !is_in_global_block(path) && + (!is_in_global_block(path) || node.children.length > 1) && !path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used) ) { const children = node.children; @@ -282,13 +286,24 @@ const visitors = { const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]); remove_global_pseudo_class(global, relative_selector.combinator, context.state); - if ( - node.metadata.rule?.metadata.parent_rule && - global.args === null && - relative_selector.combinator === null - ) { - // div { :global.x { ... } } becomes div { &.x { ... } } - context.state.code.prependRight(global.start, '&'); + const parent_rule = node.metadata.rule?.metadata.parent_rule; + if (parent_rule && global.args === null) { + if (relative_selector.combinator === null) { + // div { :global.x { ... } } becomes div { &.x { ... } } + context.state.code.prependRight(global.start, '&'); + } + + // In case of multiple :global selectors in a selector list we gotta delete the comma, too, but only if + // the next selector is used; if it's unused then the comma deletion happens as part of removal of that next selector + if ( + parent_rule.prelude.children.length > 1 && + node.children.length === node.children.findIndex((s) => s === relative_selector) - 1 + ) { + const next_selector = parent_rule.prelude.children.find((s) => s.start > global.end); + if (next_selector && next_selector.metadata.used) { + context.state.code.update(global.end, next_selector.start, ''); + } + } } continue; } else { @@ -380,7 +395,9 @@ function remove_global_pseudo_class(selector, combinator, state) { // div :global.x becomes div.x while (/\s/.test(state.code.original[start - 1])) start--; } - state.code.remove(start, selector.start + ':global'.length); + + // update(...), not remove(...) because there could be a closing unused comment at the end + state.code.update(start, selector.start + ':global'.length, ''); } else { state.code .remove(selector.start, selector.start + ':global('.length) diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js new file mode 100644 index 0000000000..85dedc8012 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'css_global_block_invalid_list', + message: + "A `:global` selector cannot be part of a selector list with entries that don't contain `:global`", + position: [232, 246] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte new file mode 100644 index 0000000000..260921f704 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js new file mode 100644 index 0000000000..f24095800a --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'css_global_block_invalid_list', + message: + "A `:global` selector cannot be part of a selector list with entries that don't contain `:global`", + position: [24, 43] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte new file mode 100644 index 0000000000..2a09ec10ce --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js deleted file mode 100644 index 9ae4e758c4..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'css_global_block_invalid_list', - message: 'A `:global` selector cannot be part of a selector list with more than one item', - position: [9, 31] - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte deleted file mode 100644 index 75178bc664..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/packages/svelte/tests/css/samples/global-block/_config.js b/packages/svelte/tests/css/samples/global-block/_config.js index bee0d7204d..a8b11a73ec 100644 --- a/packages/svelte/tests/css/samples/global-block/_config.js +++ b/packages/svelte/tests/css/samples/global-block/_config.js @@ -16,6 +16,20 @@ export default test({ column: 16, character: 932 } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "unused :global"', + start: { + line: 100, + column: 29, + character: 1223 + }, + end: { + line: 100, + column: 43, + character: 1237 + } } ] }); diff --git a/packages/svelte/tests/css/samples/global-block/expected.css b/packages/svelte/tests/css/samples/global-block/expected.css index 438749224b..12f9a75032 100644 --- a/packages/svelte/tests/css/samples/global-block/expected.css +++ b/packages/svelte/tests/css/samples/global-block/expected.css @@ -90,3 +90,13 @@ opacity: 1; } } + + x, y { + color: green; + } + + div.svelte-xyz, div.svelte-xyz y /* (unused) unused*/ { + z { + color: green; + } + } diff --git a/packages/svelte/tests/css/samples/global-block/input.svelte b/packages/svelte/tests/css/samples/global-block/input.svelte index a1833636a1..ee05205d67 100644 --- a/packages/svelte/tests/css/samples/global-block/input.svelte +++ b/packages/svelte/tests/css/samples/global-block/input.svelte @@ -92,4 +92,14 @@ opacity: 1; } } + + :global x, :global y { + color: green; + } + + div :global, div :global y, unused :global { + z { + color: green; + } + } From 90563e903fd8428db4c087b6fa4f51e0200dbb45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Apr 2025 14:38:31 -0400 Subject: [PATCH 143/145] feat: add partial evaluation (#15494) * feat: add partial evaluation * fix * tweak * more * more * evaluate stuff in template * update test * SSR * unused * changeset * remove TODO * Apply suggestions from code review Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * allow unknown operators * use blocks and block-scoping in switch statement --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/selfish-onions-begin.md | 5 + .../client/visitors/RegularElement.js | 9 +- .../client/visitors/shared/utils.js | 26 +- .../server/visitors/shared/utils.js | 16 +- packages/svelte/src/compiler/phases/scope.js | 322 +++++++++++++++++- .../_expected/client/index.svelte.js | 6 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../samples/attached-sourcemap/input.svelte | 2 +- 9 files changed, 357 insertions(+), 33 deletions(-) create mode 100644 .changeset/selfish-onions-begin.md diff --git a/.changeset/selfish-onions-begin.md b/.changeset/selfish-onions-begin.md new file mode 100644 index 0000000000..decf0d5fc6 --- /dev/null +++ b/.changeset/selfish-onions-begin.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: partially evaluate certain expressions diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 45a594af1f..fa4ee9867f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -685,14 +685,13 @@ function build_element_special_value_attribute(element, node_id, attribute, cont : value ); + const evaluated = context.state.scope.evaluate(value); + const assignment = b.assignment('=', b.member(node_id, '__value'), value); + const inner_assignment = b.assignment( '=', b.member(node_id, 'value'), - b.conditional( - b.binary('==', b.null, b.assignment('=', b.member(node_id, '__value'), value)), - b.literal(''), // render null/undefined values as empty string to support placeholder options - value - ) + evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal('')) ); const update = b.stmt( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index af6e56f70c..55362d75af 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -89,21 +89,21 @@ export function build_template_chunk( } } - const is_defined = - value.type === 'BinaryExpression' || - (value.type === 'UnaryExpression' && value.operator !== 'void') || - (value.type === 'LogicalExpression' && value.right.type === 'Literal') || - (value.type === 'Identifier' && value.name === state.analysis.props_id?.name); - - if (!is_defined) { - // add `?? ''` where necessary (TODO optimise more cases) - value = b.logical('??', value, b.literal('')); - } + const evaluated = state.scope.evaluate(value); - expressions.push(value); + if (evaluated.is_known) { + quasi.value.cooked += evaluated.value + ''; + } else { + if (!evaluated.is_defined) { + // add `?? ''` where necessary + value = b.logical('??', value, b.literal('')); + } - quasi = b.quasi('', i + 1 === values.length); - quasis.push(quasi); + expressions.push(value); + + quasi = b.quasi('', i + 1 === values.length); + quasis.push(quasi); + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 2c6aa2f316..807e12a8fa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -44,15 +44,17 @@ export function process_children(nodes, { visit, state }) { if (node.type === 'Text' || node.type === 'Comment') { quasi.value.cooked += node.type === 'Comment' ? `` : escape_html(node.data); - } else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') { - if (node.expression.value != null) { - quasi.value.cooked += escape_html(node.expression.value + ''); - } } else { - expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); + const evaluated = state.scope.evaluate(node.expression); + + if (evaluated.is_known) { + quasi.value.cooked += escape_html((evaluated.value ?? '') + ''); + } else { + expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); - quasi = b.quasi('', i + 1 === sequence.length); - quasis.push(quasi); + quasi = b.quasi('', i + 1 === sequence.length); + quasis.push(quasi); + } } } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index b6063c3234..73dfeea1d9 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -16,6 +16,11 @@ import { is_reserved, is_rune } from '../../utils.js'; import { determine_slot } from '../utils/slot.js'; import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; +export const UNKNOWN = Symbol('unknown'); +/** Includes `BigInt` */ +export const NUMBER = Symbol('number'); +export const STRING = Symbol('string'); + export class Binding { /** @type {Scope} */ scope; @@ -34,7 +39,7 @@ export class Binding { * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` * @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock} */ - initial; + initial = null; /** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */ references = []; @@ -100,6 +105,264 @@ export class Binding { } } +class Evaluation { + /** @type {Set} */ + values = new Set(); + + /** + * True if there is exactly one possible value + * @readonly + * @type {boolean} + */ + is_known = true; + + /** + * True if the value is known to not be null/undefined + * @readonly + * @type {boolean} + */ + is_defined = true; + + /** + * True if the value is known to be a string + * @readonly + * @type {boolean} + */ + is_string = true; + + /** + * True if the value is known to be a number + * @readonly + * @type {boolean} + */ + is_number = true; + + /** + * @readonly + * @type {any} + */ + value = undefined; + + /** + * + * @param {Scope} scope + * @param {Expression} expression + */ + constructor(scope, expression) { + switch (expression.type) { + case 'Literal': { + this.values.add(expression.value); + break; + } + + case 'Identifier': { + const binding = scope.get(expression.name); + + if (binding) { + if ( + binding.initial?.type === 'CallExpression' && + get_rune(binding.initial, scope) === '$props.id' + ) { + this.values.add(STRING); + break; + } + + const is_prop = + binding.kind === 'prop' || + binding.kind === 'rest_prop' || + binding.kind === 'bindable_prop'; + + if (!binding.updated && binding.initial !== null && !is_prop) { + const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); + for (const value of evaluation.values) { + this.values.add(value); + } + break; + } + + // TODO each index is always defined + } + + // TODO glean what we can from reassignments + // TODO one day, expose props and imports somehow + + this.values.add(UNKNOWN); + break; + } + + case 'BinaryExpression': { + const a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in` + const b = scope.evaluate(expression.right); + + if (a.is_known && b.is_known) { + this.values.add(binary[expression.operator](a.value, b.value)); + break; + } + + switch (expression.operator) { + case '!=': + case '!==': + case '<': + case '<=': + case '>': + case '>=': + case '==': + case '===': + case 'in': + case 'instanceof': + this.values.add(true); + this.values.add(false); + break; + + case '%': + case '&': + case '*': + case '**': + case '-': + case '/': + case '<<': + case '>>': + case '>>>': + case '^': + case '|': + this.values.add(NUMBER); + break; + + case '+': + if (a.is_string || b.is_string) { + this.values.add(STRING); + } else if (a.is_number && b.is_number) { + this.values.add(NUMBER); + } else { + this.values.add(STRING); + this.values.add(NUMBER); + } + break; + + default: + this.values.add(UNKNOWN); + } + break; + } + + case 'ConditionalExpression': { + const test = scope.evaluate(expression.test); + const consequent = scope.evaluate(expression.consequent); + const alternate = scope.evaluate(expression.alternate); + + if (test.is_known) { + for (const value of (test.value ? consequent : alternate).values) { + this.values.add(value); + } + } else { + for (const value of consequent.values) { + this.values.add(value); + } + + for (const value of alternate.values) { + this.values.add(value); + } + } + break; + } + + case 'LogicalExpression': { + const a = scope.evaluate(expression.left); + const b = scope.evaluate(expression.right); + + if (a.is_known) { + if (b.is_known) { + this.values.add(logical[expression.operator](a.value, b.value)); + break; + } + + if ( + (expression.operator === '&&' && !a.value) || + (expression.operator === '||' && a.value) || + (expression.operator === '??' && a.value != null) + ) { + this.values.add(a.value); + } else { + for (const value of b.values) { + this.values.add(value); + } + } + + break; + } + + for (const value of a.values) { + this.values.add(value); + } + + for (const value of b.values) { + this.values.add(value); + } + break; + } + + case 'UnaryExpression': { + const argument = scope.evaluate(expression.argument); + + if (argument.is_known) { + this.values.add(unary[expression.operator](argument.value)); + break; + } + + switch (expression.operator) { + case '!': + case 'delete': + this.values.add(false); + this.values.add(true); + break; + + case '+': + case '-': + case '~': + this.values.add(NUMBER); + break; + + case 'typeof': + this.values.add(STRING); + break; + + case 'void': + this.values.add(undefined); + break; + + default: + this.values.add(UNKNOWN); + } + break; + } + + default: { + this.values.add(UNKNOWN); + } + } + + for (const value of this.values) { + this.value = value; // saves having special logic for `size === 1` + + if (value !== STRING && typeof value !== 'string') { + this.is_string = false; + } + + if (value !== NUMBER && typeof value !== 'number') { + this.is_number = false; + } + + if (value == null || value === UNKNOWN) { + this.is_defined = false; + } + } + + if (this.values.size > 1 || typeof this.value === 'symbol') { + this.is_known = false; + } + } +} + export class Scope { /** @type {ScopeRoot} */ root; @@ -279,8 +542,63 @@ export class Scope { this.root.conflicts.add(node.name); } } + + /** + * Does partial evaluation to find an exact value or at least the rough type of the expression. + * Only call this once scope has been fully generated in a first pass, + * else this evaluates on incomplete data and may yield wrong results. + * @param {Expression} expression + * @param {Set} values + */ + evaluate(expression, values = new Set()) { + return new Evaluation(this, expression); + } } +/** @type {Record any>} */ +const binary = { + '!=': (left, right) => left != right, + '!==': (left, right) => left !== right, + '<': (left, right) => left < right, + '<=': (left, right) => left <= right, + '>': (left, right) => left > right, + '>=': (left, right) => left >= right, + '==': (left, right) => left == right, + '===': (left, right) => left === right, + in: (left, right) => left in right, + instanceof: (left, right) => left instanceof right, + '%': (left, right) => left % right, + '&': (left, right) => left & right, + '*': (left, right) => left * right, + '**': (left, right) => left ** right, + '+': (left, right) => left + right, + '-': (left, right) => left - right, + '/': (left, right) => left / right, + '<<': (left, right) => left << right, + '>>': (left, right) => left >> right, + '>>>': (left, right) => left >>> right, + '^': (left, right) => left ^ right, + '|': (left, right) => left | right +}; + +/** @type {Record any>} */ +const unary = { + '-': (argument) => -argument, + '+': (argument) => +argument, + '!': (argument) => !argument, + '~': (argument) => ~argument, + typeof: (argument) => typeof argument, + void: () => undefined, + delete: () => true +}; + +/** @type {Record any>} */ +const logical = { + '||': (left, right) => left || right, + '&&': (left, right) => left && right, + '??': (left, right) => left ?? right +}; + export class ScopeRoot { /** @type {Set} */ conflicts = new Set(); diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index 332c909ebe..21f6ed9680 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -10,11 +10,11 @@ export default function Nullish_coallescence_omittance($$anchor) { var fragment = root(); var h1 = $.first_child(fragment); - h1.textContent = `Hello, ${name ?? ''}!`; + h1.textContent = 'Hello, world!'; var b = $.sibling(h1, 2); - b.textContent = `${1 ?? 'stuff'}${2 ?? 'more stuff'}${3 ?? 'even more stuff'}`; + b.textContent = '123'; var button = $.sibling(b, 2); @@ -26,7 +26,7 @@ export default function Nullish_coallescence_omittance($$anchor) { var h1_1 = $.sibling(button, 2); - h1_1.textContent = `Hello, ${name ?? 'earth' ?? ''}`; + h1_1.textContent = 'Hello, world'; $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); $.append($$anchor, fragment); } diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index 8181bfd98e..3b23befcd4 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -4,5 +4,5 @@ export default function Nullish_coallescence_omittance($$payload) { let name = 'world'; let count = 0; - $$payload.out += `

    Hello, ${$.escape(name)}!

    ${$.escape(1 ?? 'stuff')}${$.escape(2 ?? 'more stuff')}${$.escape(3 ?? 'even more stuff')}

    Hello, ${$.escape(name ?? 'earth' ?? null)}

    `; + $$payload.out += `

    Hello, world!

    123

    Hello, world

    `; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js index 46d376aca2..b341d39f28 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js @@ -38,7 +38,7 @@ export default function Skip_static_subtree($$anchor, $$props) { var select = $.sibling(div_1, 2); var option = $.child(select); - option.value = null == (option.__value = 'a') ? '' : 'a'; + option.value = option.__value = 'a'; $.reset(select); var img = $.sibling(select, 2); diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte index 21a47a72a9..715bbda8d9 100644 --- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte +++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte @@ -8,4 +8,4 @@ replace_me_script = 'hello' ; -

    {done_replace_script_2}

    +

    {Math.random() < 1 && done_replace_script_2}

    From 6a7e53feaa53425624a47d7ebed98ff8d6fb1d8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 22:23:50 -0400 Subject: [PATCH 144/145] Version Packages (#15764) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/green-starfishes-shave.md | 5 ----- .changeset/selfish-onions-begin.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/green-starfishes-shave.md delete mode 100644 .changeset/selfish-onions-begin.md diff --git a/.changeset/green-starfishes-shave.md b/.changeset/green-starfishes-shave.md deleted file mode 100644 index 967bba753c..0000000000 --- a/.changeset/green-starfishes-shave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: relax `:global` selector list validation diff --git a/.changeset/selfish-onions-begin.md b/.changeset/selfish-onions-begin.md deleted file mode 100644 index decf0d5fc6..0000000000 --- a/.changeset/selfish-onions-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: partially evaluate certain expressions diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 58f2317796..c8f0ad7ed9 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.27.0 + +### Minor Changes + +- feat: partially evaluate certain expressions ([#15494](https://github.com/sveltejs/svelte/pull/15494)) + +### Patch Changes + +- fix: relax `:global` selector list validation ([#15762](https://github.com/sveltejs/svelte/pull/15762)) + ## 5.26.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b8654671ec..af78d2679a 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.26.3", + "version": "5.27.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 1909588146..27a39136f8 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.26.3'; +export const VERSION = '5.27.0'; export const PUBLIC_VERSION = '5'; From e079ac92b869a8405f2e76dd7a3d830cd3629c55 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Tue, 15 Apr 2025 19:58:51 -0600 Subject: [PATCH 145/145] fix: Throw on unrendered snippets in `dev` (#15766) --- .changeset/strong-pianos-promise.md | 5 +++ .../98-reference/.generated/shared-errors.md | 37 +++++++++++++++++++ .../svelte/messages/shared-errors/errors.md | 35 ++++++++++++++++++ .../server/visitors/SnippetBlock.js | 25 ++++++++----- .../server/visitors/shared/component.js | 9 ++++- .../src/internal/client/dom/blocks/snippet.js | 7 +++- packages/svelte/src/internal/client/index.js | 3 +- packages/svelte/src/internal/server/index.js | 3 +- packages/svelte/src/internal/shared/errors.js | 15 ++++++++ .../svelte/src/internal/shared/validate.js | 12 ++++++ .../_config.js | 8 ++++ .../main.svelte | 5 +++ .../_config.js | 3 ++ .../main.svelte | 5 +++ .../_config.js | 3 ++ .../main.svelte | 5 +++ .../unrendered-children.svelte | 5 +++ .../_config.js | 8 ++++ .../main.svelte | 5 +++ .../unrendered-children.svelte | 5 +++ .../_expected/server/index.svelte.js | 4 +- 21 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 .changeset/strong-pianos-promise.md create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte diff --git a/.changeset/strong-pianos-promise.md b/.changeset/strong-pianos-promise.md new file mode 100644 index 0000000000..f5214c7dcb --- /dev/null +++ b/.changeset/strong-pianos-promise.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: Throw on unrendered snippets in `dev` diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 4c81d7b894..6c31aaafd0 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -60,6 +60,43 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +### snippet_without_render_tag + +``` +Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`. +``` + +A component throwing this error will look something like this (`children` is not being rendered): + +```svelte + + +{children} +``` + +...or like this (a parent component is passing a snippet where a non-snippet value is expected): + +```svelte + + + {#snippet label()} + Hi! + {/snippet} + +``` + +```svelte + + + + +

    {label}

    +``` + ### store_invalid_shape ``` diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 20f3d193d9..4b4d332202 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -52,6 +52,41 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +## snippet_without_render_tag + +> Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`. + +A component throwing this error will look something like this (`children` is not being rendered): + +```svelte + + +{children} +``` + +...or like this (a parent component is passing a snippet where a non-snippet value is expected): + +```svelte + + + {#snippet label()} + Hi! + {/snippet} + +``` + +```svelte + + + + +

    {label}

    +``` + ## store_invalid_shape > `%name%` is not a store with a `subscribe` method diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js index cae3e7d79c..a67fcc8885 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement } from 'estree' */ +/** @import { ArrowFunctionExpression, BlockStatement, CallExpression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import { dev } from '../../../../state.js'; @@ -9,20 +9,27 @@ import * as b from '../../../../utils/builders.js'; * @param {ComponentContext} context */ export function SnippetBlock(node, context) { - const fn = b.function_declaration( - node.expression, - [b.id('$$payload'), ...node.parameters], - /** @type {BlockStatement} */ (context.visit(node.body)) - ); + const body = /** @type {BlockStatement} */ (context.visit(node.body)); + if (dev) { - fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload')))); + body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload')))); } + + /** @type {ArrowFunctionExpression | CallExpression} */ + let fn = b.arrow([b.id('$$payload'), ...node.parameters], body); + + if (dev) { + fn = b.call('$.prevent_snippet_stringification', fn); + } + + const declaration = b.declaration('const', [b.declarator(node.expression, fn)]); + // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone fn.___snippet = true; if (node.metadata.can_hoist) { - context.state.hoisted.push(fn); + context.state.hoisted.push(declaration); } else { - context.state.init.push(fn); + context.state.init.push(declaration); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 695161ff9b..f4b3dd1b09 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -4,6 +4,7 @@ import { empty_comment, build_attribute_value } from './utils.js'; import * as b from '../../../../../utils/builders.js'; import { is_element_node } from '../../../../nodes.js'; +import { dev } from '../../../../../state.js'; /** * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node @@ -238,7 +239,13 @@ export function build_inline_component(node, expression, context) { ) ) { // create `children` prop... - push_prop(b.prop('init', b.id('children'), slot_fn)); + push_prop( + b.prop( + 'init', + b.id('children'), + dev ? b.call('$.prevent_snippet_stringification', slot_fn) : slot_fn + ) + ); // and `$$slots.default: true` so that `` on the child works serialized_slots.push(b.init(slot_name, b.true)); diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index b916a02ce5..a48153900f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -15,6 +15,7 @@ import * as e from '../../errors.js'; import { DEV } from 'esm-env'; import { get_first_child, get_next_sibling } from '../operations.js'; import { noop } from '../../../shared/utils.js'; +import { prevent_snippet_stringification } from '../../../shared/validate.js'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -60,7 +61,7 @@ export function snippet(node, get_snippet, ...args) { * @param {(node: TemplateNode, ...args: any[]) => void} fn */ export function wrap_snippet(component, fn) { - return (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => { + const snippet = (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => { var previous_component_function = dev_current_component_function; set_dev_current_component_function(component); @@ -70,6 +71,10 @@ export function wrap_snippet(component, fn) { set_dev_current_component_function(previous_component_function); } }; + + prevent_snippet_stringification(snippet); + + return snippet; } /** diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index e977bf3b0f..14d6e29f5b 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -157,7 +157,8 @@ export { invalid_default_snippet, validate_dynamic_element_tag, validate_store, - validate_void_dynamic_element + validate_void_dynamic_element, + prevent_snippet_stringification } from '../shared/validate.js'; export { strict_equals, equals } from './dev/equality.js'; export { log_if_contains_state } from './dev/console-log.js'; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index d711778a44..b58a1d4372 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -509,7 +509,8 @@ export { fallback } from '../shared/utils.js'; export { invalid_default_snippet, validate_dynamic_element_tag, - validate_void_dynamic_element + validate_void_dynamic_element, + prevent_snippet_stringification } from '../shared/validate.js'; export { escape_html as escape }; diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 2e89dc1ad1..b8606fbf6f 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -48,6 +48,21 @@ export function lifecycle_outside_component(name) { } } +/** + * Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`. + * @returns {never} + */ +export function snippet_without_render_tag() { + if (DEV) { + const error = new Error(`snippet_without_render_tag\nAttempted to render a snippet without a \`{@render}\` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change \`{snippet}\` to \`{@render snippet()}\`.\nhttps://svelte.dev/e/snippet_without_render_tag`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/snippet_without_render_tag`); + } +} + /** * `%name%` is not a store with a `subscribe` method * @param {string} name diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index 852c0e83bf..bbb237594b 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -35,3 +35,15 @@ export function validate_store(store, name) { e.store_invalid_shape(name); } } + +/** + * @template {() => unknown} T + * @param {T} fn + */ +export function prevent_snippet_stringification(fn) { + fn.toString = () => { + e.snippet_without_render_tag(); + return ''; + }; + return fn; +} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js new file mode 100644 index 0000000000..94c5de10af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + runtime_error: 'snippet_without_render_tag' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte new file mode 100644 index 0000000000..3f8edfe4fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-dev/main.svelte @@ -0,0 +1,5 @@ +{testSnippet} + +{#snippet testSnippet()} +

    hi again

    +{/snippet} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte new file mode 100644 index 0000000000..3f8edfe4fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-block-without-render-tag-prod/main.svelte @@ -0,0 +1,5 @@ +{testSnippet} + +{#snippet testSnippet()} +

    hi again

    +{/snippet} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte new file mode 100644 index 0000000000..4a4ed3176f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/main.svelte @@ -0,0 +1,5 @@ + + +Hi diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte new file mode 100644 index 0000000000..6b7154a5a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev-prod/unrendered-children.svelte @@ -0,0 +1,5 @@ + + +{children} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js new file mode 100644 index 0000000000..94c5de10af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + runtime_error: 'snippet_without_render_tag' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte new file mode 100644 index 0000000000..4a4ed3176f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/main.svelte @@ -0,0 +1,5 @@ + + +Hi diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte new file mode 100644 index 0000000000..6b7154a5a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-children-without-render-tag-dev/unrendered-children.svelte @@ -0,0 +1,5 @@ + + +{children} diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js index cadae2cf15..04bfbf6ae4 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js @@ -1,9 +1,9 @@ import * as $ from 'svelte/internal/server'; import TextInput from './Child.svelte'; -function snippet($$payload) { +const snippet = ($$payload) => { $$payload.out += `Something`; -} +}; export default function Bind_component_snippet($$payload) { let value = '';