Merge branch 'main' into cleanup-a11y

pull/16345/head
ComputerGuy 2 months ago
commit b630e62dac

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: silence autofocus a11y warning inside `<dialog>`

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: replace inline regex with variable

@ -8,9 +8,17 @@ jobs:
trigger:
runs-on: ubuntu-latest
if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
permissions:
issues: write # to add / delete reactions
pull-requests: write # to read PR data, and to add labels
actions: read # to check workflow status
contents: read # to clone the repo
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- uses: actions/github-script@v6
- name: monitor action permissions
uses: GitHubSecurityLab/actions-permissions/monitor@v1
- name: check user authorization # user needs triage permission
uses: actions/github-script@v7
id: check-permissions
with:
script: |
const user = context.payload.sender.login
@ -29,7 +37,7 @@ jobs:
}
if (hasTriagePermission) {
console.log('Allowed')
console.log('User is allowed. Adding +1 reaction.')
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
@ -37,16 +45,18 @@ jobs:
content: '+1',
})
} else {
console.log('Not allowed')
console.log('User is not allowed. Adding -1 reaction.')
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: '-1',
})
throw new Error('not allowed')
throw new Error('User does not have the necessary permissions.')
}
- uses: actions/github-script@v6
- name: Get PR Data
uses: actions/github-script@v7
id: get-pr-data
with:
script: |
@ -59,21 +69,27 @@ jobs:
return {
num: context.issue.number,
branchName: pr.head.ref,
commit: pr.head.sha,
repo: pr.head.repo.full_name
}
- id: generate-token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 #keep pinned for security reasons, currently 1.8.0
- name: Generate Token
id: generate-token
uses: actions/create-github-app-token@v2
with:
app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
repository: '${{ github.repository_owner }}/svelte-ecosystem-ci'
- uses: actions/github-script@v6
app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
repositories: |
svelte
svelte-ecosystem-ci
- name: Trigger Downstream Workflow
uses: actions/github-script@v7
id: trigger
env:
COMMENT: ${{ github.event.comment.body }}
with:
github-token: ${{ steps.generate-token.outputs.token }}
result-encoding: string
script: |
const comment = process.env.COMMENT.trim()
const prData = ${{ steps.get-pr-data.outputs.result }}
@ -89,6 +105,7 @@ jobs:
prNumber: '' + prData.num,
branchName: prData.branchName,
repo: prData.repo,
commit: prData.commit,
suite: suite === '' ? '-' : suite
}
})

@ -74,6 +74,12 @@ Effect cannot be created inside a `$derived` value that was not itself created i
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
```
### get_abort_signal_outside_reaction
```
`getAbortSignal()` can only be called inside an effect or derived
```
### hydration_failed
```

@ -1,5 +1,63 @@
# svelte
## 5.35.6
### Patch Changes
- chore: simplify reaction/source ownership tracking ([#16333](https://github.com/sveltejs/svelte/pull/16333))
- chore: simplify internal component `pop()` ([#16331](https://github.com/sveltejs/svelte/pull/16331))
## 5.35.5
### Patch Changes
- fix: associate sources in Spring/Tween/SvelteMap/SvelteSet with correct reaction ([#16325](https://github.com/sveltejs/svelte/pull/16325))
- fix: re-evaluate derived props during teardown ([#16278](https://github.com/sveltejs/svelte/pull/16278))
## 5.35.4
### Patch Changes
- fix: abort and reschedule effect processing after state change in user effect ([#16280](https://github.com/sveltejs/svelte/pull/16280))
## 5.35.3
### Patch Changes
- fix: account for mounting when `select_option` in `attribute_effect` ([#16309](https://github.com/sveltejs/svelte/pull/16309))
- fix: do not proxify the value assigned to a derived ([#16302](https://github.com/sveltejs/svelte/pull/16302))
## 5.35.2
### Patch Changes
- fix: bump esrap ([#16295](https://github.com/sveltejs/svelte/pull/16295))
## 5.35.1
### Patch Changes
- feat: add parent hierarchy to `__svelte_meta` objects ([#16255](https://github.com/sveltejs/svelte/pull/16255))
## 5.35.0
### Minor Changes
- feat: add `getAbortSignal()` ([#16266](https://github.com/sveltejs/svelte/pull/16266))
### Patch Changes
- chore: simplify props ([#16270](https://github.com/sveltejs/svelte/pull/16270))
## 5.34.9
### Patch Changes
- fix: ensure unowned deriveds can add themselves as reactions while connected ([#16249](https://github.com/sveltejs/svelte/pull/16249))
## 5.34.8
### Patch Changes

@ -48,6 +48,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
## get_abort_signal_outside_reaction
> `getAbortSignal()` can only be called inside an effect or derived
## hydration_failed
> Failed to hydrate the application

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.34.8",
"version": "5.35.6",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -164,14 +164,14 @@
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"@sveltejs/acorn-typescript": "^1.0.5",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.4.8",
"esrap": "^2.1.0",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -1,9 +1,14 @@
/** @import { Node } from 'esrap/languages/ts' */
/** @import * as ESTree from 'estree' */
/** @import { AST } from 'svelte/compiler' */
// @ts-check
import process from 'node:process';
import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
import ts from 'esrap/languages/ts';
const DIR = '../../documentation/docs/98-reference/.generated';
@ -97,79 +102,49 @@ function run() {
.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
.replace(/\r\n/g, '\n');
/**
* @type {Array<{
* type: string;
* value: string;
* start: number;
* end: number
* }>}
*/
/** @type {AST.JSComment[]} */
const comments = [];
let ast = acorn.parse(source, {
let ast = /** @type {ESTree.Node} */ (
/** @type {unknown} */ (
acorn.parse(source, {
ecmaVersion: 'latest',
sourceType: 'module',
onComment: (block, value, start, end) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
locations: true,
onComment: comments
})
)
);
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
comments.forEach((comment) => {
if (comment.type === 'Block') {
comment.value = comment.value.replace(/^\t+/gm, '');
}
});
ast = walk(ast, null, {
_(node, { next }) {
let comment;
while (comments[0] && comments[0].start < node.start) {
comment = comments.shift();
// @ts-expect-error
(node.leadingComments ||= []).push(comment);
}
next();
if (comments[0]) {
const slice = source.slice(node.end, comments[0].start);
if (/^[,) \t]*$/.test(slice)) {
// @ts-expect-error
node.trailingComments = [comments.shift()];
}
}
},
// @ts-expect-error
Identifier(node, context) {
if (node.name === 'CODES') {
return {
/** @type {ESTree.ArrayExpression} */
const array = {
type: 'ArrayExpression',
elements: Object.keys(messages[name]).map((code) => ({
type: 'Literal',
value: code
}))
};
return array;
}
}
});
if (comments.length > 0) {
// @ts-expect-error
(ast.trailingComments ||= []).push(...comments);
}
const body = /** @type {ESTree.Program} */ (ast).body;
const category = messages[name];
// find the `export function CODE` node
const index = ast.body.findIndex((node) => {
const index = body.findIndex((node) => {
if (
node.type === 'ExportNamedDeclaration' &&
node.declaration &&
@ -181,8 +156,19 @@ function run() {
if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);
const template_node = ast.body[index];
ast.body.splice(index, 1);
const template_node = body[index];
body.splice(index, 1);
const jsdoc = /** @type {AST.JSComment} */ (
comments.findLast((comment) => comment.start < /** @type {number} */ (template_node.start))
);
const printed = esrap.print(
/** @type {Node} */ (ast),
ts({
comments: comments.filter((comment) => comment !== jsdoc)
})
);
for (const code in category) {
const { messages } = category[code];
@ -203,7 +189,7 @@ function run() {
};
});
/** @type {import('estree').Expression} */
/** @type {ESTree.Expression} */
let message = { type: 'Literal', value: '' };
let prev_vars;
@ -221,10 +207,10 @@ function run() {
const parts = text.split(/(%\w+%)/);
/** @type {import('estree').Expression[]} */
/** @type {ESTree.Expression[]} */
const expressions = [];
/** @type {import('estree').TemplateElement[]} */
/** @type {ESTree.TemplateElement[]} */
const quasis = [];
for (let i = 0; i < parts.length; i += 1) {
@ -244,7 +230,7 @@ function run() {
}
}
/** @type {import('estree').Expression} */
/** @type {ESTree.Expression} */
const expression = {
type: 'TemplateLiteral',
expressions,
@ -272,42 +258,8 @@ function run() {
prev_vars = vars;
}
const clone = walk(/** @type {import('estree').Node} */ (template_node), null, {
// @ts-expect-error Block is a block comment, which is not recognised
Block(node, context) {
if (!node.value.includes('PARAMETER')) return;
const value = /** @type {string} */ (node.value)
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
return messages[messages.length - 1]
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
}
if (line.includes('PARAMETER')) {
return vars
.map((name, i) => {
const optional = i >= group[0].vars.length;
return optional
? ` * @param {string | undefined | null} [${name}]`
: ` * @param {string} ${name}`;
})
.join('\n');
}
return line;
})
.filter((x) => x !== '')
.join('\n');
if (value !== node.value) {
return { ...node, value };
}
},
const clone = /** @type {ESTree.Statement} */ (
walk(/** @type {ESTree.Node} */ (template_node), null, {
FunctionDeclaration(node, context) {
if (node.id.name !== 'CODE') return;
@ -321,8 +273,8 @@ function run() {
}
}
return /** @type {import('estree').FunctionDeclaration} */ ({
.../** @type {import('estree').FunctionDeclaration} */ (context.next()),
return /** @type {ESTree.FunctionDeclaration} */ ({
.../** @type {ESTree.FunctionDeclaration} */ (context.next()),
params,
id: {
...node.id,
@ -331,7 +283,7 @@ function run() {
});
},
TemplateLiteral(node, context) {
/** @type {import('estree').TemplateElement} */
/** @type {ESTree.TemplateElement} */
let quasi = {
type: 'TemplateElement',
value: {
@ -340,7 +292,7 @@ function run() {
tail: node.quasis[0].tail
};
/** @type {import('estree').TemplateLiteral} */
/** @type {ESTree.TemplateLiteral} */
let out = {
type: 'TemplateLiteral',
quasis: [quasi],
@ -375,7 +327,7 @@ function run() {
}
out.quasis.push((quasi = q));
out.expressions.push(/** @type {import('estree').Expression} */ (context.visit(e)));
out.expressions.push(/** @type {ESTree.Expression} */ (context.visit(e)));
}
return out;
@ -392,18 +344,54 @@ function run() {
if (node.name !== 'MESSAGE') return;
return message;
}
});
})
);
// @ts-expect-error
ast.body.push(clone);
const jsdoc_clone = {
...jsdoc,
value: /** @type {string} */ (jsdoc.value)
.split('\n')
.map((line) => {
if (line === ' * MESSAGE') {
return messages[messages.length - 1]
.split('\n')
.map((line) => ` * ${line}`)
.join('\n');
}
const module = esrap.print(ast);
if (line.includes('PARAMETER')) {
return vars
.map((name, i) => {
const optional = i >= group[0].vars.length;
return optional
? ` * @param {string | undefined | null} [${name}]`
: ` * @param {string} ${name}`;
})
.join('\n');
}
return line;
})
.filter((x) => x !== '')
.join('\n')
};
const block = esrap.print(
// @ts-expect-error some bullshit
/** @type {ESTree.Program} */ ({ ...ast, body: [clone] }),
ts({ comments: [jsdoc_clone] })
).code;
printed.code += `\n\n${block}`;
body.push(clone);
}
fs.writeFileSync(
dest,
`/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
module.code,
printed.code,
'utf-8'
);
}

@ -15,10 +15,12 @@ class InternalCompileError extends Error {
constructor(code, message, position) {
super(message);
this.stack = ''; // avoid unnecessary noise; don't set it as a class property or it becomes enumerable
// We want to extend from Error so that various bundler plugins properly handle it.
// But we also want to share the same object shape with that of warnings, therefore
// we create an instance of the shared class an copy over its properties.
this.#diagnostic = new CompileDiagnostic(code, message, position);
Object.assign(this, this.#diagnostic);
this.name = 'CompileError';
}
@ -816,7 +818,9 @@ export function bind_invalid_expression(node) {
* @returns {never}
*/
export function bind_invalid_name(node, name, explanation) {
e(node, 'bind_invalid_name', `${explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`);
e(node, 'bind_invalid_name', `${explanation
? `\`bind:${name}\` is not a valid binding. ${explanation}`
: `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`);
}
/**

@ -3,7 +3,6 @@
/** @import { AST } from './public.js' */
import { walk as zimmerframe_walk } from 'zimmerframe';
import { convert } from './legacy.js';
import { parse as parse_acorn } from './phases/1-parse/acorn.js';
import { parse as _parse } from './phases/1-parse/index.js';
import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js';
import { analyze_component, analyze_module } from './phases/2-analyze/index.js';
@ -21,9 +20,8 @@ export { default as preprocess } from './preprocess/index.js';
*/
export function compile(source, options) {
source = remove_bom(source);
state.reset_warning_filter(options.warningFilter);
state.reset_warnings(options.warningFilter);
const validated = validate_component_options(options, '');
state.reset(source, validated);
let parsed = _parse(source);
@ -65,11 +63,10 @@ export function compile(source, options) {
*/
export function compileModule(source, options) {
source = remove_bom(source);
state.reset_warning_filter(options.warningFilter);
state.reset_warnings(options.warningFilter);
const validated = validate_module_options(options, '');
state.reset(source, validated);
const analysis = analyze_module(parse_acorn(source, false), validated);
const analysis = analyze_module(source, validated);
return transform_module(analysis, source, validated);
}
@ -97,6 +94,7 @@ export function compileModule(source, options) {
* @returns {Record<string, any>}
*/
// TODO 6.0 remove unused `filename`
/**
* The parse function parses a component, returning only its abstract syntax tree.
*
@ -105,14 +103,15 @@ export function compileModule(source, options) {
*
* The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile.
*
* The `filename` option is unused and will be removed in Svelte 6.0.
*
* @param {string} source
* @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options]
* @returns {AST.Root | LegacyRoot}
*/
export function parse(source, { filename, rootDir, modern, loose } = {}) {
export function parse(source, { modern, loose } = {}) {
source = remove_bom(source);
state.reset_warning_filter(() => false);
state.reset(source, { filename: filename ?? '(unknown)', rootDir });
state.reset_warnings(() => false);
const ast = _parse(source, loose);
return to_public_ast(source, ast, modern);

@ -451,6 +451,7 @@ export function convert(source, ast) {
SpreadAttribute(node) {
return { ...node, type: 'Spread' };
},
// @ts-ignore
StyleSheet(node, context) {
return {
...node,

@ -9,7 +9,7 @@ import { parse } from '../phases/1-parse/index.js';
import { regex_valid_component_name } from '../phases/1-parse/state/element.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js';
import { reset, reset_warning_filter } from '../state.js';
import { reset, reset_warnings } from '../state.js';
import {
extract_identifiers,
extract_all_identifiers_from_expression,
@ -134,8 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) {
return start + style_placeholder + end;
});
reset_warning_filter(() => false);
reset(source, { filename: filename ?? '(unknown)' });
reset_warnings(() => false);
let parsed = parse(source);

@ -1,18 +1,32 @@
/** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin());
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/**
* @param {string} source
* @param {AST.JSComment[]} comments
* @param {boolean} typescript
* @param {boolean} [is_script]
*/
export function parse(source, typescript, is_script) {
export function parse(source, comments, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
@ -53,13 +67,19 @@ export function parse(source, typescript, is_script) {
/**
* @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, typescript, index) {
export function parse_expression_at(source, comments, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments),
index
);
const ast = parser.parseExpressionAt(source, index, {
onComment,
@ -78,26 +98,20 @@ export function parse_expression_at(source, typescript, index) {
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source
* @param {CommentWithLocation[]} comments
* @param {number} index
*/
function get_comment_handlers(source) {
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/** @type {CommentWithLocation[]} */
const comments = [];
function get_comment_handlers(source, comments, index = 0) {
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @param {number} end
* @param {import('acorn').Position} [start_loc]
* @param {import('acorn').Position} [end_loc]
*/
onComment: (block, value, start, end) => {
onComment: (block, value, start, end, start_loc, end_loc) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
@ -109,13 +123,26 @@ function get_comment_handlers(source) {
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
comments.push({
type: block ? 'Block' : 'Line',
value,
start,
end,
loc: {
start: /** @type {import('acorn').Position} */ (start_loc),
end: /** @type {import('acorn').Position} */ (end_loc)
}
});
},
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) {
if (comments.length === 0) return;
comments = comments
.filter((comment) => comment.start >= index)
.map(({ type, value, start, end }) => ({ type, value, start, end }));
walk(ast, null, {
_(node, { next, path }) {
let comment;

@ -1,4 +1,5 @@
/** @import { AST } from '#compiler' */
/** @import { Comment } from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
@ -8,6 +9,7 @@ import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
@ -87,6 +89,7 @@ export class Parser {
type: 'Root',
fragment: create_fragment(),
options: null,
comments: [],
metadata: {
ts: this.ts
}
@ -299,6 +302,8 @@ export class Parser {
* @returns {AST.Root}
*/
export function parse(template, loose = false) {
state.set_source(template);
const parser = new Parser(template, loose);
return parser.root;
}

@ -59,7 +59,12 @@ export default function read_pattern(parser) {
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ (
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1)
parse_expression_at(
`${space_with_newline}(${pattern_string} = 1)`,
parser.root.comments,
parser.ts,
start - 1
)
).left;
expression.typeAnnotation = read_type_annotation(parser);
@ -96,13 +101,13 @@ function read_type_annotation(parser) {
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.ts, a);
let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.ts, a);
expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that

@ -34,12 +34,24 @@ export function get_loose_identifier(parser, opening_token) {
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
const node = parse_expression_at(parser.template, parser.ts, parser.index);
let comment_index = parser.root.comments.length;
const node = parse_expression_at(
parser.template,
parser.root.comments,
parser.ts,
parser.index
);
let num_parens = 0;
if (node.leadingComments !== undefined && node.leadingComments.length > 0) {
parser.index = node.leadingComments.at(-1).end;
let i = parser.root.comments.length;
while (i-- > comment_index) {
const comment = parser.root.comments[i];
if (comment.end < node.start) {
parser.index = comment.end;
break;
}
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
@ -47,9 +59,9 @@ export default function read_expression(parser, opening_token, disallow_loose) {
}
let index = /** @type {number} */ (node.end);
if (node.trailingComments !== undefined && node.trailingComments.length > 0) {
index = node.trailingComments.at(-1).end;
}
const last_comment = parser.root.comments.at(-1);
if (last_comment && last_comment.end > index) index = last_comment.end;
while (num_parens > 0) {
const char = parser.template[index];

@ -34,7 +34,7 @@ export function read_script(parser, start, attributes) {
let ast;
try {
ast = acorn.parse(source, parser.ts, true);
ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}

@ -398,7 +398,12 @@ function open(parser) {
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
parse_expression_at(
prelude + `${params} => {}`,
parser.root.comments,
parser.ts,
params_start
)
)
: { params: [] };

@ -1,8 +1,9 @@
/** @import { Expression, Node, Program } from 'estree' */
/** @import { Comment, Expression, Node, Program } from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { extract_identifiers } from '../../utils/ast.js';
@ -75,6 +76,7 @@ import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclarator } from './visitors/VariableDeclarator.js';
import is_reference from 'is-reference';
import { mark_subtree_dynamic } from './visitors/shared/fragment.js';
import * as state from '../../state.js';
/**
* @type {Visitors}
@ -231,11 +233,17 @@ function get_component_name(filename) {
const RESERVED = ['$$props', '$$restProps', '$$slots'];
/**
* @param {Program} ast
* @param {string} source
* @param {ValidatedModuleCompileOptions} options
* @returns {Analysis}
*/
export function analyze_module(ast, options) {
export function analyze_module(source, options) {
/** @type {AST.JSComment[]} */
const comments = [];
state.set_source(source);
const ast = parse(source, comments, false, false);
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null);
for (const [name, references] of scope.references) {
@ -259,9 +267,17 @@ export function analyze_module(ast, options) {
runes: true,
immutable: true,
tracing: false,
comments,
classes: new Map()
};
state.reset({
dev: options.dev,
filename: options.filename,
rootDir: options.rootDir,
runes: true
});
walk(
/** @type {Node} */ (ast),
{
@ -429,6 +445,7 @@ export function analyze_component(root, source, options) {
module,
instance,
template,
comments: root.comments,
elements: [],
runes,
// if we are not in runes mode but we have no reserved references ($$props, $$restProps)
@ -498,6 +515,14 @@ export function analyze_component(root, source, options) {
snippets: new Set()
};
state.reset({
component_name: analysis.name,
dev: options.dev,
filename: options.filename,
rootDir: options.rootDir,
runes: true
});
if (!runes) {
// every exported `let` or `var` declaration becomes a prop, everything else becomes an export
for (const node of instance.ast.body) {

@ -168,9 +168,9 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
update: /** @type {any} */ (null),
expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null)
template: /** @type {any} */ (null),
memoizer: /** @type {any} */ (null)
};
const module = /** @type {ESTree.Program} */ (
@ -362,6 +362,9 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (template.body)
]);
// trick esrap into including comments
component_block.loc = instance.loc;
if (!analysis.runes) {
// Bind static exports to props so that people can access them with bind:x
for (const { name, alias } of analysis.exports) {

@ -12,6 +12,7 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { Template } from './transform-template/template.js';
import type { Memoizer } from './visitors/shared/utils.js';
export interface ClientTransformState extends TransformState {
/**
@ -49,8 +50,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Expressions used inside the render effect */
readonly expressions: Expression[];
/** Memoized expressions */
readonly memoizer: Memoizer;
/** The HTML template string */
readonly template: Template;
readonly metadata: {

@ -137,6 +137,7 @@ function build_assignment(operator, left, right, context) {
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'derived' &&
binding.kind !== 'store_sub' &&
context.state.analysis.runes &&
should_proxy(right, context.state.scope) &&

@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@ -54,7 +54,7 @@ export function AwaitBlock(node, context) {
}
context.state.init.push(
b.stmt(
add_svelte_meta(
b.call(
'$.await',
context.state.node,
@ -64,7 +64,9 @@ export function AwaitBlock(node, context) {
: b.null,
then_block,
catch_block
)
),
node,
'await'
)
);
}

@ -13,7 +13,7 @@ import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -337,7 +337,7 @@ export function EachBlock(node, context) {
);
}
context.state.init.push(b.stmt(b.call('$.each', ...args)));
context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each'));
}
/**

@ -6,7 +6,7 @@ import * as b from '#compiler/builders';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
import { Template } from '../transform-template/template.js';
/**
@ -64,8 +64,8 @@ export function Fragment(node, context) {
...context.state,
init: [],
update: [],
expressions: [],
after_update: [],
memoizer: new Memoizer(),
template: new Template(),
transform: { ...context.state.transform },
metadata: {

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -74,7 +74,7 @@ export function IfBlock(node, context) {
args.push(b.id('$$elseif'));
}
statements.push(b.stmt(b.call('$.if', ...args)));
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
context.state.init.push(b.block(statements));
}

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
@ -15,6 +15,10 @@ export function KeyBlock(node, context) {
const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push(
b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)))
add_svelte_meta(
b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)),
node,
'key'
)
);
}

@ -22,13 +22,7 @@ import {
build_set_style
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import {
build_render_statement,
build_template_chunk,
build_update_assignment,
get_expression_id,
memoize_expression
} from './shared/utils.js';
import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
/**
@ -200,9 +194,6 @@ export function RegularElement(node, context) {
const node_id = context.state.node;
if (has_spread) {
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
} else {
/** If true, needs `__value` for inputs */
const needs_special_value_handling =
node.name === 'option' ||
@ -210,6 +201,9 @@ export function RegularElement(node, context) {
bindings.has('group') ||
bindings.has('checked');
if (has_spread) {
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
} else {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) {
visit_event_attribute(attribute, context);
@ -217,7 +211,6 @@ export function RegularElement(node, context) {
}
if (needs_special_value_handling && attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
continue;
}
@ -260,8 +253,7 @@ export function RegularElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) =>
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
(value, metadata) => (metadata.has_call ? context.state.memoizer.add(value) : value)
);
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@ -392,6 +384,15 @@ export function RegularElement(node, context) {
context.state.update.push(b.stmt(b.assignment('=', dir, dir)));
}
if (!has_spread && needs_special_value_handling) {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
break;
}
}
}
context.state.template.pop_element();
}
@ -453,11 +454,15 @@ function setup_select_synchronization(value_binding, context) {
/**
* @param {AST.ClassDirective[]} class_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context
* @param {Memoizer} memoizer
* @return {ObjectExpression | Identifier}
*/
export function build_class_directives_object(class_directives, expressions, context) {
export function build_class_directives_object(
class_directives,
context,
memoizer = context.state.memoizer
) {
let properties = [];
let has_call_or_state = false;
@ -469,38 +474,40 @@ export function build_class_directives_object(class_directives, expressions, con
const directives = b.object(properties);
return has_call_or_state ? get_expression_id(expressions, directives) : directives;
return has_call_or_state ? memoizer.add(directives) : directives;
}
/**
* @param {AST.StyleDirective[]} style_directives
* @param {Expression[]} expressions
* @param {ComponentContext} context
* @return {ObjectExpression | ArrayExpression}}
* @param {Memoizer} memoizer
* @return {ObjectExpression | ArrayExpression | Identifier}}
*/
export function build_style_directives_object(style_directives, expressions, context) {
let normal_properties = [];
let important_properties = [];
export function build_style_directives_object(
style_directives,
context,
memoizer = context.state.memoizer
) {
const normal = b.object([]);
const important = b.object([]);
let has_call_or_state = false;
for (const directive of style_directives) {
for (const d 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(expressions, value) : value
).value;
const property = b.init(directive.name, expression);
if (directive.modifiers.includes('important')) {
important_properties.push(property);
} else {
normal_properties.push(property);
}
d.value === true
? build_getter(b.id(d.name), context.state)
: build_attribute_value(d.value, context).value;
const object = d.modifiers.includes('important') ? important : normal;
object.properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
}
return important_properties.length
? b.array([b.object(normal_properties), b.object(important_properties)])
: b.object(normal_properties);
const directives = important.properties.length ? b.array([normal, important]) : normal;
return has_call_or_state ? memoizer.add(directives) : directives;
}
/**
@ -622,12 +629,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
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(state, value)
: get_expression_id(state.expressions, value)
: value
metadata.has_call ? state.memoizer.add(value) : value
);
const evaluated = context.state.scope.evaluate(value);
@ -652,23 +654,21 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
: inner_assignment
);
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
}
if (has_state) {
const id = state.scope.generate(`${node_id.name}_value`);
build_update_assignment(
state,
id,
const id = b.id(state.scope.generate(`${node_id.name}_value`));
// `<option>` is a special case: The value property reflects to the DOM. If the value is set to undefined,
// that means the value should be set to the empty string. To be able to do that when the value is
// initially undefined, we need to set a value that is guaranteed to be different.
element === 'option' ? b.object([]) : undefined,
value,
update
);
const init = element === 'option' ? b.object([]) : undefined;
state.init.push(b.var(id, init));
state.update.push(b.if(b.binary('!==', id, b.assignment('=', id, value)), b.block([update])));
} else {
state.init.push(update);
}
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id)));
}
}

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
import { add_svelte_meta, build_expression } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -48,16 +48,22 @@ export function RenderTag(node, context) {
}
context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
add_svelte_meta(
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
node,
'render'
)
);
} else {
context.state.init.push(
b.stmt(
add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
context.state.node,
...args
)
),
node,
'render'
)
);
}

@ -10,7 +10,7 @@ import {
build_attribute_effect,
build_set_class
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -46,8 +46,8 @@ export function SvelteElement(node, context) {
node: element_id,
init: [],
update: [],
expressions: [],
after_update: []
after_update: [],
memoizer: new Memoizer()
}
};

@ -4,7 +4,12 @@
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
import {
build_bind_this,
memoize_expression,
validate_binding,
add_svelte_meta
} from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js';
@ -483,7 +488,8 @@ export function build_component(node, component_name, context) {
);
} else {
context.state.template.push_comment();
statements.push(b.stmt(fn(anchor)));
statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name }));
}
return statements.length > 1 ? b.block(statements) : statements[0];

@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_expression, build_template_chunk, get_expression_id } from './utils.js';
import { build_expression, build_template_chunk, Memoizer } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -28,18 +28,12 @@ export function build_attribute_effect(
/** @type {ObjectExpression['properties']} */
const values = [];
/** @type {Expression[]} */
const expressions = [];
/** @param {Expression} value */
function memoize(value) {
return b.id(`$${expressions.push(value) - 1}`);
}
const memoizer = new Memoizer();
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? memoize(value) : value
metadata.has_call ? memoizer.add(value) : value
);
if (
@ -57,7 +51,7 @@ export function build_attribute_effect(
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call) {
value = memoize(value);
value = memoizer.add(value);
}
values.push(b.spread(value));
@ -69,7 +63,7 @@ export function build_attribute_effect(
b.prop(
'init',
b.array([b.id('$.CLASS')]),
build_class_directives_object(class_directives, expressions, context)
build_class_directives_object(class_directives, context, memoizer)
)
);
}
@ -79,21 +73,20 @@ export function build_attribute_effect(
b.prop(
'init',
b.array([b.id('$.STYLE')]),
build_style_directives_object(style_directives, expressions, context)
build_style_directives_object(style_directives, context, memoizer)
)
);
}
const ids = memoizer.apply();
context.state.init.push(
b.stmt(
b.call(
'$.attribute_effect',
element_id,
b.arrow(
expressions.map((_, i) => b.id(`$${i}`)),
b.object(values)
),
expressions.length > 0 && b.array(expressions.map((expression) => b.thunk(expression))),
b.arrow(ids, b.object(values)),
memoizer.sync_values(),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
@ -158,7 +151,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value);
}
return metadata.has_call ? get_expression_id(context.state.expressions, value) : value;
return metadata.has_call ? context.state.memoizer.add(value) : value;
});
/** @type {Identifier | undefined} */
@ -171,7 +164,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
let next;
if (class_directives.length) {
next = build_class_directives_object(class_directives, context.state.expressions, context);
next = build_class_directives_object(class_directives, context);
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {
@ -226,7 +219,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/
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.expressions, value) : value
metadata.has_call ? context.state.memoizer.add(value) : value
);
/** @type {Identifier | undefined} */
@ -235,11 +228,11 @@ export function build_set_style(node_id, attribute, style_directives, context) {
/** @type {ObjectExpression | Identifier | undefined} */
let prev;
/** @type {ArrayExpression | ObjectExpression | undefined} */
/** @type {Expression | undefined} */
let next;
if (style_directives.length) {
next = build_style_directives_object(style_directives, context.state.expressions, context);
next = build_style_directives_object(style_directives, context);
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {

@ -1,4 +1,4 @@
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe';
@ -7,7 +7,7 @@ import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator } from '../../../../../state.js';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter, create_derived } from '../../utils.js';
/**
@ -21,12 +21,41 @@ export function memoize_expression(state, value) {
}
/**
* Pushes `value` into `expressions` and returns a new id
* @param {Expression[]} expressions
* @param {Expression} value
* A utility for extracting complex expressions (such as call expressions)
* from templates and replacing them with `$0`, `$1` etc
*/
export function get_expression_id(expressions, value) {
return b.id(`$${expressions.push(value) - 1}`);
export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#sync = [];
/**
* @param {Expression} expression
*/
add(expression) {
const id = b.id('#'); // filled in later
this.#sync.push({ id, expression });
return id;
}
apply() {
return this.#sync.map((memo, i) => {
memo.id.name = `$${i}`;
return memo.id;
});
}
deriveds(runes = true) {
return this.#sync.map((memo) =>
b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression)))
);
}
sync_values() {
if (this.#sync.length === 0) return;
return b.array(this.#sync.map((memo) => b.thunk(memo.expression)));
}
}
/**
@ -40,8 +69,7 @@ export function build_template_chunk(
values,
context,
state = context.state,
memoize = (value, metadata) =>
metadata.has_call ? get_expression_id(state.expressions, value) : value
memoize = (value, metadata) => (metadata.has_call ? state.memoizer.add(value) : value)
) {
/** @type {Expression[]} */
const expressions = [];
@ -128,18 +156,20 @@ export function build_template_chunk(
* @param {ComponentClientTransformState} state
*/
export function build_render_statement(state) {
const ids = state.memoizer.apply();
const values = state.memoizer.sync_values();
return b.stmt(
b.call(
'$.template_effect',
b.arrow(
state.expressions.map((_, i) => b.id(`$${i}`)),
ids,
state.update.length === 1 && state.update[0].type === 'ExpressionStatement'
? state.update[0].expression
: b.block(state.update)
),
state.expressions.length > 0 &&
b.array(state.expressions.map((expression) => b.thunk(expression))),
state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal')
values,
values && !state.analysis.runes && b.id('$.derived_safe_equal')
)
);
}
@ -165,20 +195,6 @@ export function parse_directive_name(name) {
return expression;
}
/**
* @param {ComponentClientTransformState} state
* @param {string} id
* @param {Expression | undefined} init
* @param {Expression} value
* @param {ExpressionStatement} update
*/
export function build_update_assignment(state, id, init, value, update) {
state.init.push(b.var(id, init));
state.update.push(
b.if(b.binary('!==', b.id(id), b.assignment('=', b.id(id), value)), b.block([update]))
);
}
/**
* Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression | SequenceExpression} expression
@ -408,3 +424,34 @@ export function build_expression(context, expression, metadata, state = context.
return sequence;
}
/**
* Wraps a statement/expression with dev stack tracking in dev mode
* @param {Expression} expression - The function call to wrap (e.g., $.if, $.each, etc.)
* @param {{ start?: number }} node - AST node for location info
* @param {'component' | 'if' | 'each' | 'await' | 'key' | 'render'} type - Type of block/component
* @param {Record<string, number | string>} [additional] - Any additional properties to add to the dev stack entry
* @returns {ExpressionStatement} - Statement with or without dev stack wrapping
*/
export function add_svelte_meta(expression, node, type, additional) {
if (!dev) {
return b.stmt(expression);
}
const location = node.start !== undefined && locator(node.start);
if (!location) {
return b.stmt(expression);
}
return b.stmt(
b.call(
'$.add_svelte_meta',
b.arrow([], expression),
b.literal(type),
b.id(component_name),
b.literal(location.line),
b.literal(location.column),
additional && b.object(Object.entries(additional).map(([k, v]) => b.init(k, b.literal(v))))
)
);
}

@ -1,6 +1,8 @@
/** @import { Node } from 'esrap/languages/ts' */
/** @import { ValidatedCompileOptions, CompileResult, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { ComponentAnalysis, Analysis } from '../types' */
import { print } from 'esrap';
import ts from 'esrap/languages/ts';
import { VERSION } from '../../../version.js';
import { server_component, server_module } from './server/transform-server.js';
import { client_component, client_module } from './client/transform-client.js';
@ -34,7 +36,7 @@ export function transform_component(analysis, source, options) {
const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte');
const js = print(program, {
const js = print(/** @type {Node} */ (program), ts({ comments: analysis.comments }), {
// include source content; makes it easier/more robust looking up the source map code
// (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source,
@ -93,13 +95,19 @@ export function transform_module(analysis, source, options) {
];
}
return {
js: print(program, {
const js = print(/** @type {Node} */ (program), ts({ comments: analysis.comments }), {
// include source content; makes it easier/more robust looking up the source map code
// (else esrap does return null for source and sourceMapContent which may trip up tooling)
sourceMapContent: source,
sourceMapSource: get_source_name(options.filename, undefined, 'input.svelte.js')
}),
});
// prepend comment
js.code = `/* ${basename} generated by Svelte v${VERSION} */\n${js.code}`;
js.map.mappings = ';' + js.map.mappings;
return {
js,
css: null,
metadata: {
runes: true

@ -242,6 +242,9 @@ export function server_component(analysis, options) {
.../** @type {Statement[]} */ (template.body)
]);
// trick esrap into including comments
component_block.loc = instance.loc;
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(

@ -2,6 +2,7 @@ import type { AST, Binding, StateField } from '#compiler';
import type {
AssignmentExpression,
ClassBody,
Comment,
Identifier,
LabeledStatement,
Node,
@ -33,10 +34,13 @@ export interface ReactiveStatement {
*/
export interface Analysis {
module: Js;
/** @deprecated use `component_name` from `state.js` instead */
name: string; // TODO should this be filename? it's used in `compileModule` as well as `compile`
/** @deprecated use `runes` from `state.js` instead */
runes: boolean;
immutable: boolean;
tracing: boolean;
comments: AST.JSComment[];
classes: Map<ClassBody, Map<string, StateField>>;
@ -88,6 +92,7 @@ export interface ComponentAnalysis extends Analysis {
keyframes: string[];
has_global: boolean;
};
/** @deprecated use `source` from `state.js` instead */
source: string;
undefined_exports: Map<string, Node>;
/**

@ -16,6 +16,11 @@ export let warnings = [];
*/
export let filename;
/**
* The name of the component that is used in the `export default function ...` statement.
*/
export let component_name = '<unknown>';
/**
* The original source code
* @type {string}
@ -28,8 +33,16 @@ export let source;
*/
export let dev;
export let runes = false;
export let locator = getLocator('', { offsetLine: 1 });
/** @param {string} value */
export function set_source(value) {
source = value;
locator = getLocator(source, { offsetLine: 1 });
}
/**
* @param {AST.SvelteNode & { start?: number | undefined }} node
*/
@ -71,8 +84,9 @@ export function pop_ignore() {
*
* @param {(warning: Warning) => boolean} fn
*/
export function reset_warning_filter(fn = () => true) {
export function reset_warnings(fn = () => true) {
warning_filter = fn;
warnings = [];
}
/**
@ -85,23 +99,27 @@ export function is_ignored(node, code) {
}
/**
* @param {string} _source
* @param {{ dev?: boolean; filename: string; rootDir?: string }} options
* @param {{
* dev: boolean;
* filename: string;
* component_name?: string;
* rootDir?: string;
* runes: boolean;
* }} state
*/
export function reset(_source, options) {
source = _source;
const root_dir = options.rootDir?.replace(/\\/g, '/');
filename = options.filename.replace(/\\/g, '/');
export function reset(state) {
const root_dir = state.rootDir?.replace(/\\/g, '/');
filename = state.filename.replace(/\\/g, '/');
dev = !!options.dev;
dev = state.dev;
runes = state.runes;
component_name = state.component_name ?? '(unknown)';
if (typeof root_dir === 'string' && filename.startsWith(root_dir)) {
// make filename relative to rootDir
filename = filename.replace(root_dir, '').replace(/^[/\\]/, '');
}
locator = getLocator(source, { offsetLine: 1 });
warnings = [];
ignore_stack = [];
ignore_map.clear();
}

@ -72,6 +72,8 @@ export namespace AST {
instance: Script | null;
/** The parsed `<script module>` element, if exists */
module: Script | null;
/** Comments found in <script> and {expressions} */
comments: JSComment[];
/** @internal */
metadata: {
/** Whether the component was parsed with typescript */
@ -537,6 +539,17 @@ export namespace AST {
attributes: Attribute[];
}
export interface JSComment {
type: 'Line' | 'Block';
value: string;
start: number;
end: number;
loc: {
start: { line: number; column: number };
end: { line: number; column: number };
};
}
export type AttributeLike = Attribute | SpreadAttribute | Directive;
export type Directive =
@ -593,7 +606,7 @@ export namespace AST {
| AST.Comment
| Block;
export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node;
export type SvelteNode = Node | TemplateNode | AST.Fragment | _CSS.Node | Script;
export type { _CSS as CSS };
}

@ -398,10 +398,13 @@ export function merge_with_preprocessor_map(result, options, source_name) {
// map may contain a different file name. Patch our map beforehand to align sources so merging
// with the preprocessor map works correctly.
result.map.sources = [file_basename];
result.map = apply_preprocessor_sourcemap(
Object.assign(
result.map,
apply_preprocessor_sourcemap(
file_basename,
result.map,
/** @type {any} */ (options.sourcemap)
)
);
// After applying the preprocessor map, we need to do the inverse and make the sources
// relative to the input file again in case the output code is in a different directory.

@ -1,12 +1,6 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
import {
warnings,
ignore_stack,
ignore_map,
warning_filter
} from './state.js';
import { warnings, ignore_stack, ignore_map, warning_filter } from './state.js';
import { CompileDiagnostic } from './utils/compile_diagnostic.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */
@ -40,6 +34,7 @@ function w(node, code, message) {
const warning = new InternalCompileWarning(code, message, node && node.start !== undefined ? [node.start, node.end ?? node.start] : undefined);
if (!warning_filter(warning)) return;
warnings.push(warning);
}
@ -496,7 +491,9 @@ export function a11y_role_supports_aria_props_implicit(node, attribute, role, na
* @param {string | undefined | null} [suggestion]
*/
export function a11y_unknown_aria_attribute(node, attribute, suggestion) {
w(node, 'a11y_unknown_aria_attribute', `${suggestion ? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?` : `Unknown aria attribute 'aria-${attribute}'`}\nhttps://svelte.dev/e/a11y_unknown_aria_attribute`);
w(node, 'a11y_unknown_aria_attribute', `${suggestion
? `Unknown aria attribute 'aria-${attribute}'. Did you mean '${suggestion}'?`
: `Unknown aria attribute 'aria-${attribute}'`}\nhttps://svelte.dev/e/a11y_unknown_aria_attribute`);
}
/**
@ -506,7 +503,9 @@ export function a11y_unknown_aria_attribute(node, attribute, suggestion) {
* @param {string | undefined | null} [suggestion]
*/
export function a11y_unknown_role(node, role, suggestion) {
w(node, 'a11y_unknown_role', `${suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`);
w(node, 'a11y_unknown_role', `${suggestion
? `Unknown role '${role}'. Did you mean '${suggestion}'?`
: `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`);
}
/**
@ -534,7 +533,9 @@ export function legacy_code(node, code, suggestion) {
* @param {string | undefined | null} [suggestion]
*/
export function unknown_code(node, code, suggestion) {
w(node, 'unknown_code', `${suggestion ? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)` : `\`${code}\` is not a recognised code`}\nhttps://svelte.dev/e/unknown_code`);
w(node, 'unknown_code', `${suggestion
? `\`${code}\` is not a recognised code (did you mean \`${suggestion}\`?)`
: `\`${code}\` is not a recognised code`}\nhttps://svelte.dev/e/unknown_code`);
}
/**

@ -1,7 +1,7 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.js' */
import { untrack } from './internal/client/runtime.js';
import { active_reaction, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
@ -44,6 +44,37 @@ if (DEV) {
throw_rune_error('$bindable');
}
/**
* Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed.
*
* Must be called while a derived or effect is running.
*
* ```svelte
* <script>
* import { getAbortSignal } from 'svelte';
*
* let { id } = $props();
*
* async function getData(id) {
* const response = await fetch(`/items/${id}`, {
* signal: getAbortSignal()
* });
*
* return await response.json();
* }
*
* const data = $derived(await getData(id));
* </script>
* ```
*/
export function getAbortSignal() {
if (active_reaction === null) {
e.get_abort_signal_outside_reaction();
}
return (active_reaction.ac ??= new AbortController()).signal;
}
/**
* `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.

@ -35,6 +35,8 @@ export function unmount() {
export async function tick() {}
export { getAbortSignal } from './internal/server/abort-signal.js';
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
export { createRawSnippet } from './internal/server/blocks/snippet.js';

@ -15,18 +15,23 @@ export const DESTROYED = 1 << 14;
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 16;
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
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 INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18;
export const EFFECT_PRESERVED = 1 << 19;
export const EFFECT_IS_UPDATING = 1 << 20;
export const USER_EFFECT = 1 << 21;
export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
/** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */
export const STALE_REACTION = new (class StaleReactionError extends Error {
name = 'StaleReactionError';
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
})();
export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const COMMENT_NODE = 8;

@ -1,4 +1,4 @@
/** @import { ComponentContext } from '#client' */
/** @import { ComponentContext, DevStackEntry } from '#client' */
import { DEV } from 'esm-env';
import { lifecycle_outside_component } from '../shared/errors.js';
@ -9,8 +9,9 @@ import {
set_active_effect,
set_active_reaction
} from './runtime.js';
import { effect, teardown } from './reactivity/effects.js';
import { create_user_effect, teardown } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
/** @type {ComponentContext | null} */
export let component_context = null;
@ -20,6 +21,43 @@ export function set_component_context(context) {
component_context = context;
}
/** @type {DevStackEntry | null} */
export let dev_stack = null;
/** @param {DevStackEntry | null} stack */
export function set_dev_stack(stack) {
dev_stack = stack;
}
/**
* Execute a callback with a new dev stack entry
* @param {() => any} callback - Function to execute
* @param {DevStackEntry['type']} type - Type of block/component
* @param {any} component - Component function
* @param {number} line - Line number
* @param {number} column - Column number
* @param {Record<string, any>} [additional] - Any additional properties to add to the dev stack entry
* @returns {any}
*/
export function add_svelte_meta(callback, type, component, line, column, additional) {
const parent = dev_stack;
dev_stack = {
type,
file: component[FILENAME],
line,
column,
parent,
...additional
};
try {
return callback();
} finally {
dev_stack = parent;
}
}
/**
* The current component function. Different from current component context:
* ```html
@ -101,16 +139,14 @@ export function getAllContexts() {
* @returns {void}
*/
export function push(props, runes = false, fn) {
var ctx = (component_context = {
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 = {
@ -121,10 +157,6 @@ export function push(props, runes = false, fn) {
};
}
teardown(() => {
/** @type {ComponentContext} */ (ctx).d = true;
});
if (DEV) {
// component function
component_context.function = fn;
@ -138,37 +170,28 @@ export function push(props, runes = false, fn) {
* @returns {T}
*/
export function pop(component) {
const context_stack_item = component_context;
if (context_stack_item !== null) {
if (component !== undefined) {
context_stack_item.x = component;
}
const component_effects = context_stack_item.e;
if (component_effects !== null) {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
context_stack_item.e = null;
try {
for (var i = 0; i < component_effects.length; i++) {
var component_effect = component_effects[i];
set_active_effect(component_effect.effect);
set_active_reaction(component_effect.reaction);
effect(component_effect.fn);
var context = /** @type {ComponentContext} */ (component_context);
var effects = context.e;
if (effects !== null) {
context.e = null;
for (var fn of effects) {
create_user_effect(fn);
}
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
}
if (component !== undefined) {
context.x = component;
}
component_context = context_stack_item.p;
component_context = context.p;
if (DEV) {
dev_current_component_function = context_stack_item.p?.function ?? null;
}
context_stack_item.m = true;
dev_current_component_function = component_context?.function ?? null;
}
// Micro-optimization: Don't set .a above to the empty object
// so it can be garbage-collected when the return here is unused
return component || /** @type {T} */ ({});
return component ?? /** @type {T} */ ({});
}
/** @returns {boolean} */

@ -2,6 +2,7 @@
import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants';
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js';
import { dev_stack } from '../context.js';
/**
* @param {any} fn
@ -28,6 +29,7 @@ export function add_locations(fn, filename, locations) {
function assign_location(element, filename, location) {
// @ts-expect-error
element.__svelte_meta = {
parent: dev_stack,
loc: { file: filename, line: location[0], column: location[1] }
};

@ -16,9 +16,11 @@ import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import {
component_context,
dev_stack,
is_runes,
set_component_context,
set_dev_current_component_function
set_dev_current_component_function,
set_dev_stack
} from '../../context.js';
const PENDING = 0;
@ -45,6 +47,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {any} */
var component_function = DEV ? component_context?.function : null;
var dev_original_stack = DEV ? dev_stack : null;
/** @type {V | Promise<V> | typeof UNINITIALIZED} */
var input = UNINITIALIZED;
@ -75,7 +78,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
set_active_effect(effect);
set_active_reaction(effect); // TODO do we need both?
set_component_context(active_component_context);
if (DEV) set_dev_current_component_function(component_function);
if (DEV) {
set_dev_current_component_function(component_function);
set_dev_stack(dev_original_stack);
}
}
try {
@ -107,7 +113,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
}
} finally {
if (restore) {
if (DEV) set_dev_current_component_function(null);
if (DEV) {
set_dev_current_component_function(null);
set_dev_stack(null);
}
set_component_context(null);
set_active_reaction(null);
set_active_effect(null);

@ -1,6 +1,5 @@
/** @import { Effect, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants';
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { component_context, set_component_context } from '../../context.js';
import { invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
@ -21,67 +20,124 @@ import {
import { queue_micro_task } from '../task.js';
/**
* @param {Effect} boundary
* @param {() => void} fn
* @typedef {{
* onerror?: (error: unknown, reset: () => void) => void;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
* }} BoundaryProps
*/
var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
/**
* @param {TemplateNode} node
* @param {BoundaryProps} props
* @param {((anchor: Node) => void)} children
* @returns {void}
*/
export function boundary(node, props, children) {
new Boundary(node, props, children);
}
export class Boundary {
/** @type {TemplateNode} */
#anchor;
/** @type {TemplateNode} */
#hydrate_open;
/** @type {BoundaryProps} */
#props;
/** @type {((anchor: Node) => void)} */
#children;
/** @type {Effect} */
#effect;
/** @type {Effect | null} */
#main_effect = null;
/** @type {Effect | null} */
#failed_effect = null;
#is_creating_fallback = false;
/**
* @param {TemplateNode} node
* @param {BoundaryProps} props
* @param {((anchor: Node) => void)} children
*/
function with_boundary(boundary, fn) {
constructor(node, props, children) {
this.#anchor = node;
this.#props = props;
this.#children = children;
this.#hydrate_open = hydrate_node;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
if (hydrating) {
hydrate_next();
}
try {
this.#main_effect = branch(() => children(this.#anchor));
} catch (error) {
this.error(error);
}
}, flags);
if (hydrating) {
this.#anchor = hydrate_node;
}
}
/**
* @param {() => Effect | null} fn
*/
#run(fn) {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
var previous_ctx = component_context;
set_active_effect(boundary);
set_active_reaction(boundary);
set_component_context(boundary.ctx);
set_active_effect(this.#effect);
set_active_reaction(this.#effect);
set_component_context(this.#effect.ctx);
try {
fn();
return fn();
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_ctx);
}
}
}
/**
* @param {TemplateNode} node
* @param {{
* onerror?: (error: unknown, reset: () => void) => void,
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
* }} props
* @param {((anchor: Node) => void)} boundary_fn
* @returns {void}
*/
export function boundary(node, props, boundary_fn) {
var anchor = node;
/** @param {unknown} error */
error(error) {
var onerror = this.#props.onerror;
let failed = this.#props.failed;
/** @type {Effect} */
var boundary_effect;
block(() => {
var boundary = /** @type {Effect} */ (active_effect);
var hydrate_open = hydrate_node;
var is_creating_fallback = false;
const reset = () => {
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
});
}
// We re-use the effect's fn property to avoid allocation of an additional field
boundary.fn = (/** @type {unknown}} */ error) => {
var onerror = props.onerror;
let failed = props.failed;
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
return branch(() => this.#children(this.#anchor));
});
};
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if ((!onerror && !failed) || is_creating_fallback) {
if (this.#is_creating_fallback || (!onerror && !failed)) {
throw error;
}
var reset = () => {
pause_effect(boundary_effect);
with_boundary(boundary, () => {
is_creating_fallback = false;
boundary_effect = branch(() => boundary_fn(anchor));
});
};
var previous_reaction = active_reaction;
try {
@ -91,46 +147,43 @@ export function boundary(node, props, boundary_fn) {
set_active_reaction(previous_reaction);
}
if (boundary_effect) {
destroy_effect(boundary_effect);
} else if (hydrating) {
set_hydrate_node(hydrate_open);
if (this.#main_effect) {
destroy_effect(this.#main_effect);
this.#main_effect = null;
}
if (this.#failed_effect) {
destroy_effect(this.#failed_effect);
this.#failed_effect = null;
}
if (hydrating) {
set_hydrate_node(this.#hydrate_open);
next();
set_hydrate_node(remove_nodes());
}
if (failed) {
// Render the `failed` snippet in a microtask
queue_micro_task(() => {
with_boundary(boundary, () => {
is_creating_fallback = true;
this.#failed_effect = this.#run(() => {
this.#is_creating_fallback = true;
try {
boundary_effect = branch(() => {
return branch(() => {
failed(
anchor,
this.#anchor,
() => error,
() => reset
);
});
} catch (error) {
invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent));
invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
return null;
} finally {
this.#is_creating_fallback = false;
}
is_creating_fallback = false;
});
});
}
};
if (hydrating) {
hydrate_next();
}
boundary_effect = branch(() => boundary_fn(anchor));
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT);
if (hydrating) {
anchor = hydrate_node;
}
}

@ -18,7 +18,7 @@ import {
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
import { active_effect } from '../../runtime.js';
import { component_context } from '../../context.js';
import { component_context, dev_stack } from '../../context.js';
import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { assign_nodes } from '../template.js';
@ -107,6 +107,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
if (DEV && location) {
// @ts-expect-error
element.__svelte_meta = {
parent: dev_stack,
loc: {
file: filename,
line: location[0],

@ -20,7 +20,7 @@ import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
import { block, branch, destroy_effect } from '../../reactivity/effects.js';
import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
import { derived } from '../../reactivity/deriveds.js';
import { init_select, select_option } from './bindings/select.js';
@ -491,7 +491,7 @@ export function attribute_effect(
var current = set_attributes(element, prev, next, css_hash, skip_warning);
if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value, false);
select_option(/** @type {HTMLSelectElement} */ (element), next.value);
}
for (let symbol of Object.getOwnPropertySymbols(effects)) {
@ -513,10 +513,12 @@ export function attribute_effect(
});
if (is_select) {
init_select(
/** @type {HTMLSelectElement} */ (element),
() => /** @type {Record<string | symbol, any>} */ (prev).value
);
var select = /** @type {HTMLSelectElement} */ (element);
effect(() => {
select_option(select, /** @type {Record<string | symbol, any>} */ (prev).value, true);
init_select(select);
});
}
inited = true;

@ -1,6 +1,5 @@
import { effect } from '../../../reactivity/effects.js';
import { effect, teardown } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js';
import { untrack } from '../../../runtime.js';
import { is } from '../../../proxy.js';
import { is_array } from '../../../../shared/utils.js';
import * as w from '../../../warnings.js';
@ -10,9 +9,9 @@ import * as w from '../../../warnings.js';
* @template V
* @param {HTMLSelectElement} select
* @param {V} value
* @param {boolean} [mounting]
* @param {boolean} mounting
*/
export function select_option(select, value, mounting) {
export function select_option(select, value, mounting = false) {
if (select.multiple) {
// If value is null or undefined, keep the selection as is
if (value == undefined) {
@ -51,22 +50,12 @@ export function select_option(select, value, mounting) {
* current selection to the dom when it changes. Such
* changes could for example occur when options are
* inside an `#each` block.
* @template V
* @param {HTMLSelectElement} select
* @param {() => V} [get_value]
*/
export function init_select(select, get_value) {
let mounting = true;
effect(() => {
if (get_value) {
select_option(select, untrack(get_value), mounting);
}
mounting = false;
export function init_select(select) {
var observer = new MutationObserver(() => {
// @ts-ignore
var value = select.__value;
select_option(select, value);
select_option(select, select.__value);
// Deliberately don't update the potential binding value,
// the model should be preserved unless explicitly changed
});
@ -82,9 +71,8 @@ export function init_select(select, get_value) {
attributeFilter: ['value']
});
return () => {
teardown(() => {
observer.disconnect();
};
});
}
@ -136,7 +124,6 @@ export function bind_select_value(select, get, set = get) {
mounting = false;
});
// don't pass get_value, we already initialize it in the effect above
init_select(select);
}

@ -209,21 +209,14 @@ export function transition(flags, element, get_fn, get_params) {
var outro;
function get_options() {
var previous_reaction = active_reaction;
var previous_effect = active_effect;
set_active_reaction(null);
set_active_effect(null);
try {
return without_reactive_context(() => {
// If a transition is still ongoing, we use the existing options rather than generating
// new ones. This ensures that reversible transitions reverse smoothly, rather than
// jumping to a new spot because (for example) a different `duration` was used
return (current_options ??= get_fn()(element, get_params?.() ?? /** @type {P} */ ({}), {
direction
}));
} finally {
set_active_reaction(previous_reaction);
set_active_effect(previous_effect);
}
});
}
/** @type {TransitionManager} */

@ -1,4 +1,5 @@
/** @import { Effect } from '#client' */
/** @import { Boundary } from './dom/blocks/boundary.js' */
import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js';
import { is_firefox } from './dom/operations.js';
@ -39,8 +40,7 @@ export function invoke_error_boundary(error, effect) {
while (effect !== null) {
if ((effect.f & BOUNDARY_EFFECT) !== 0) {
try {
// @ts-expect-error
effect.fn(error);
/** @type {Boundary} */ (effect.b).error(error);
return;
} catch {}
}

@ -11,6 +11,7 @@ export function bind_invalid_checkbox_value() {
const error = new Error(`bind_invalid_checkbox_value\nUsing \`bind:value\` together with a checkbox input is not allowed. Use \`bind:checked\` instead\nhttps://svelte.dev/e/bind_invalid_checkbox_value`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_invalid_checkbox_value`);
@ -29,6 +30,7 @@ export function bind_invalid_export(component, key, name) {
const error = new Error(`bind_invalid_export\nComponent ${component} has an export named \`${key}\` that a consumer component is trying to access using \`bind:${key}\`, which is disallowed. Instead, use \`bind:this\` (e.g. \`<${name} bind:this={component} />\`) and then access the property on the bound component instance (e.g. \`component.${key}\`)\nhttps://svelte.dev/e/bind_invalid_export`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_invalid_export`);
@ -47,6 +49,7 @@ export function bind_not_bindable(key, component, name) {
const error = new Error(`bind_not_bindable\nA component is attempting to bind to a non-bindable property \`${key}\` belonging to ${component} (i.e. \`<${name} bind:${key}={...}>\`). To mark a property as bindable: \`let { ${key} = $bindable() } = $props()\`\nhttps://svelte.dev/e/bind_not_bindable`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/bind_not_bindable`);
@ -64,6 +67,7 @@ export function component_api_changed(method, component) {
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;
} else {
throw new Error(`https://svelte.dev/e/component_api_changed`);
@ -81,6 +85,7 @@ export function component_api_invalid_new(component, name) {
const error = new Error(`component_api_invalid_new\nAttempted to instantiate ${component} with \`new ${name}\`, which is no longer valid in Svelte 5. If this component is not under your control, set the \`compatibility.componentApi\` compiler option to \`4\` to keep it working.\nhttps://svelte.dev/e/component_api_invalid_new`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/component_api_invalid_new`);
@ -96,6 +101,7 @@ export function derived_references_self() {
const error = new Error(`derived_references_self\nA derived value cannot reference itself recursively\nhttps://svelte.dev/e/derived_references_self`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/derived_references_self`);
@ -111,9 +117,12 @@ export function derived_references_self() {
*/
export function each_key_duplicate(a, b, value) {
if (DEV) {
const error = new Error(`each_key_duplicate\n${value ? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}` : `Keyed each block has duplicate key at indexes ${a} and ${b}`}\nhttps://svelte.dev/e/each_key_duplicate`);
const error = new Error(`each_key_duplicate\n${value
? `Keyed each block has duplicate key \`${value}\` at indexes ${a} and ${b}`
: `Keyed each block has duplicate key at indexes ${a} and ${b}`}\nhttps://svelte.dev/e/each_key_duplicate`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/each_key_duplicate`);
@ -130,6 +139,7 @@ export function effect_in_teardown(rune) {
const error = new Error(`effect_in_teardown\n\`${rune}\` cannot be used inside an effect cleanup function\nhttps://svelte.dev/e/effect_in_teardown`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_in_teardown`);
@ -145,6 +155,7 @@ export function effect_in_unowned_derived() {
const error = new Error(`effect_in_unowned_derived\nEffect cannot be created inside a \`$derived\` value that was not itself created inside an effect\nhttps://svelte.dev/e/effect_in_unowned_derived`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_in_unowned_derived`);
@ -161,6 +172,7 @@ export function effect_orphan(rune) {
const error = new Error(`effect_orphan\n\`${rune}\` can only be used inside an effect (e.g. during component initialisation)\nhttps://svelte.dev/e/effect_orphan`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_orphan`);
@ -176,12 +188,29 @@ export function effect_update_depth_exceeded() {
const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops\nhttps://svelte.dev/e/effect_update_depth_exceeded`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/effect_update_depth_exceeded`);
}
}
/**
* `getAbortSignal()` can only be called inside an effect or derived
* @returns {never}
*/
export function get_abort_signal_outside_reaction() {
if (DEV) {
const error = new Error(`get_abort_signal_outside_reaction\n\`getAbortSignal()\` can only be called inside an effect or derived\nhttps://svelte.dev/e/get_abort_signal_outside_reaction`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`);
}
}
/**
* Failed to hydrate the application
* @returns {never}
@ -191,6 +220,7 @@ export function hydration_failed() {
const error = new Error(`hydration_failed\nFailed to hydrate the application\nhttps://svelte.dev/e/hydration_failed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/hydration_failed`);
@ -206,6 +236,7 @@ export function invalid_snippet() {
const error = new Error(`invalid_snippet\nCould not \`{@render}\` snippet due to the expression being \`null\` or \`undefined\`. Consider using optional chaining \`{@render snippet?.()}\`\nhttps://svelte.dev/e/invalid_snippet`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_snippet`);
@ -222,6 +253,7 @@ export function lifecycle_legacy_only(name) {
const error = new Error(`lifecycle_legacy_only\n\`${name}(...)\` cannot be used in runes mode\nhttps://svelte.dev/e/lifecycle_legacy_only`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/lifecycle_legacy_only`);
@ -238,6 +270,7 @@ export function props_invalid_value(key) {
const error = new Error(`props_invalid_value\nCannot do \`bind:${key}={undefined}\` when \`${key}\` has a fallback value\nhttps://svelte.dev/e/props_invalid_value`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/props_invalid_value`);
@ -254,6 +287,7 @@ export function props_rest_readonly(property) {
const error = new Error(`props_rest_readonly\nRest element properties of \`$props()\` such as \`${property}\` are readonly\nhttps://svelte.dev/e/props_rest_readonly`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/props_rest_readonly`);
@ -270,6 +304,7 @@ export function rune_outside_svelte(rune) {
const error = new Error(`rune_outside_svelte\nThe \`${rune}\` rune is only available inside \`.svelte\` and \`.svelte.js/ts\` files\nhttps://svelte.dev/e/rune_outside_svelte`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/rune_outside_svelte`);
@ -285,6 +320,7 @@ export function state_descriptors_fixed() {
const error = new Error(`state_descriptors_fixed\nProperty descriptors defined on \`$state\` objects must contain \`value\` and always be \`enumerable\`, \`configurable\` and \`writable\`.\nhttps://svelte.dev/e/state_descriptors_fixed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_descriptors_fixed`);
@ -300,6 +336,7 @@ export function state_prototype_fixed() {
const error = new Error(`state_prototype_fixed\nCannot set prototype of \`$state\` object\nhttps://svelte.dev/e/state_prototype_fixed`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_prototype_fixed`);
@ -315,6 +352,7 @@ export function state_unsafe_mutation() {
const error = new Error(`state_unsafe_mutation\nUpdating state inside \`$derived(...)\`, \`$inspect(...)\` or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/state_unsafe_mutation`);

@ -1,6 +1,6 @@
export { createAttachmentKey as attachment } from '../../attachments/index.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { push, pop } from './context.js';
export { push, pop, add_svelte_meta } from './context.js';
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';

@ -1,6 +1,13 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js';
import {
get,
active_effect,
update_version,
active_reaction,
set_update_version,
set_active_reaction
} from './runtime.js';
import {
array_prototype,
get_descriptor,
@ -8,7 +15,7 @@ import {
is_array,
object_prototype
} from '../shared/utils.js';
import { state as source, set } from './reactivity/sources.js';
import { state as source, set, increment } from './reactivity/sources.js';
import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
@ -41,7 +48,7 @@ export function proxy(value) {
var version = source(0);
var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null;
var reaction = active_reaction;
var parent_version = update_version;
/**
* Executes the proxy in the context of the reaction it was originally created in, if any
@ -49,13 +56,23 @@ export function proxy(value) {
* @param {() => T} fn
*/
var with_parent = (fn) => {
var previous_reaction = active_reaction;
set_active_reaction(reaction);
if (update_version === parent_version) {
return fn();
}
// child source is being created after the initial proxy —
// prevent it from being associated with the current reaction
var reaction = active_reaction;
var version = update_version;
set_active_reaction(null);
set_update_version(parent_version);
/** @type {T} */
var result = fn();
set_active_reaction(previous_reaction);
set_active_reaction(reaction);
set_update_version(version);
return result;
};
@ -118,7 +135,7 @@ export function proxy(value) {
if (prop in target) {
const s = with_parent(() => source(UNINITIALIZED, stack));
sources.set(prop, s);
update_version(version);
increment(version);
if (DEV) {
tag(s, get_label(path, prop));
@ -136,7 +153,7 @@ export function proxy(value) {
}
}
set(s, UNINITIALIZED);
update_version(version);
increment(version);
}
return true;
@ -304,7 +321,7 @@ export function proxy(value) {
}
}
update_version(version);
increment(version);
}
return true;
@ -343,14 +360,6 @@ function get_label(path, prop) {
return /^\d+$/.test(prop) ? `${path}[${prop}]` : `${path}['${prop}']`;
}
/**
* @param {Source<number>} signal
* @param {1 | -1} [d]
*/
function update_version(signal, d = 1) {
set(signal, signal.v + d);
}
/**
* @param {any} value
*/

@ -1,6 +1,6 @@
/** @import { Derived, Effect } from '#client' */
import { DEV } from 'esm-env';
import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '#client/constants';
import { CLEAN, DERIVED, DIRTY, EFFECT_PRESERVED, MAYBE_DIRTY, UNOWNED } from '#client/constants';
import {
active_reaction,
active_effect,
@ -19,6 +19,7 @@ import { inspect_effects, set_inspect_effects } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { tracing_mode_flag } from '../../flags/index.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
/**
* @template V
@ -38,7 +39,7 @@ export function derived(fn) {
} else {
// Since deriveds are evaluated lazily, any effects created inside them are
// created too late to ensure that the parent effect is added to the tree
active_effect.f |= EFFECT_HAS_DERIVED;
active_effect.f |= EFFECT_PRESERVED;
}
/** @type {Derived<V>} */
@ -51,9 +52,10 @@ export function derived(fn) {
fn,
reactions: null,
rv: 0,
v: /** @type {V} */ (null),
v: /** @type {V} */ (UNINITIALIZED),
wv: 0,
parent: parent_derived ?? active_effect
parent: parent_derived ?? active_effect,
ac: null
};
if (DEV && tracing_mode_flag) {

@ -31,8 +31,10 @@ import {
INSPECT_EFFECT,
HEAD_EFFECT,
MAYBE_DIRTY,
EFFECT_HAS_DERIVED,
BOUNDARY_EFFECT
EFFECT_PRESERVED,
BOUNDARY_EFFECT,
STALE_REACTION,
USER_EFFECT
} from '#client/constants';
import { set } from './sources.js';
import * as e from '../errors.js';
@ -40,7 +42,7 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { derived } from './deriveds.js';
import { component_context, dev_current_component_function } from '../context.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -103,10 +105,12 @@ function create_effect(type, fn, sync, push = true) {
last: null,
next: null,
parent,
b: parent && parent.b,
prev: null,
teardown: null,
transitions: null,
wv: 0
wv: 0,
ac: null
};
if (DEV) {
@ -133,7 +137,7 @@ function create_effect(type, fn, sync, push = true) {
effect.first === null &&
effect.nodes_start === null &&
effect.teardown === null &&
(effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0;
(effect.f & (EFFECT_PRESERVED | BOUNDARY_EFFECT)) === 0;
if (!inert && push) {
if (parent !== null) {
@ -175,33 +179,29 @@ export function teardown(fn) {
export function user_effect(fn) {
validate_effect('$effect');
// Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted
var defer =
active_effect !== null &&
(active_effect.f & BRANCH_EFFECT) !== 0 &&
component_context !== null &&
!component_context.m;
if (DEV) {
define_property(fn, 'name', {
value: '$effect'
});
}
if (defer) {
if (!active_reaction && active_effect && (active_effect.f & BRANCH_EFFECT) !== 0) {
// Top-level `$effect(...)` in a component — defer until mount
var context = /** @type {ComponentContext} */ (component_context);
(context.e ??= []).push({
fn,
effect: active_effect,
reaction: active_reaction
});
(context.e ??= []).push(fn);
} else {
var signal = effect(fn);
return signal;
// Everything else — create immediately
return create_user_effect(fn);
}
}
/**
* @param {() => void | (() => void)} fn
*/
export function create_user_effect(fn) {
return create_effect(EFFECT | USER_EFFECT, fn, false);
}
/**
* Internal representation of `$effect.pre(...)`
* @param {() => void | (() => void)} fn
@ -214,7 +214,7 @@ export function user_pre_effect(fn) {
value: '$effect.pre'
});
}
return render_effect(fn);
return create_effect(RENDER_EFFECT | USER_EFFECT, fn, true);
}
/** @param {() => void | (() => void)} fn */
@ -357,7 +357,11 @@ export function template_effect(fn, thunks = [], d = derived) {
* @param {number} flags
*/
export function block(fn, flags = 0) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
if (DEV) {
effect.dev_stack = dev_stack;
}
return effect;
}
/**
@ -397,6 +401,8 @@ export function destroy_effect_children(signal, remove_dom = false) {
signal.first = signal.last = null;
while (effect !== null) {
effect.ac?.abort(STALE_REACTION);
var next = effect.next;
if ((effect.f & ROOT_EFFECT) !== 0) {
@ -478,6 +484,7 @@ export function destroy_effect(effect, remove_dom = true) {
effect.fn =
effect.nodes_start =
effect.nodes_end =
effect.ac =
null;
}

@ -1,19 +1,26 @@
/** @import { Derived, Source } from './types.js' */
/** @import { ComponentContext } from '#client' */
/** @import { Derived, Effect, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES,
PROPS_IS_UPDATED
PROPS_IS_UPDATED,
UNINITIALIZED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js';
import { mutable_source, set, source, update } from './sources.js';
import { set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
import { get, captured_signals, untrack } from '../runtime.js';
import { safe_equals } from './equality.js';
import {
active_effect,
get,
is_destroying_effect,
set_active_effect,
untrack
} from '../runtime.js';
import * as e from '../errors.js';
import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
@ -93,7 +100,7 @@ export function rest_props(props, exclude, name) {
/**
* The proxy handler for legacy $$restProps and $$props
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, special: Record<string | symbol, (v?: unknown) => unknown>, version: Source<number> }>}}
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, special: Record<string | symbol, (v?: unknown) => unknown>, version: Source<number>, parent_effect: Effect }>}}
*/
const legacy_rest_props_handler = {
get(target, key) {
@ -103,6 +110,11 @@ const legacy_rest_props_handler = {
},
set(target, key, value) {
if (!(key in target.special)) {
var previous_effect = active_effect;
try {
set_active_effect(target.parent_effect);
// Handle props that can temporarily get out of sync with the parent
/** @type {Record<string, (v?: unknown) => unknown>} */
target.special[key] = prop(
@ -114,6 +126,9 @@ const legacy_rest_props_handler = {
/** @type {string} */ (key),
PROPS_IS_UPDATED
);
} finally {
set_active_effect(previous_effect);
}
}
target.special[key](value);
@ -152,7 +167,19 @@ const legacy_rest_props_handler = {
* @returns {Record<string, unknown>}
*/
export function legacy_rest_props(props, exclude) {
return new Proxy({ props, exclude, special: {}, version: source(0) }, legacy_rest_props_handler);
return new Proxy(
{
props,
exclude,
special: {},
version: source(0),
// TODO this is only necessary because we need to track component
// destruction inside `prop`, because of `bind:this`, but it
// seems likely that we can simplify `bind:this` instead
parent_effect: /** @type {Effect} */ (active_effect)
},
legacy_rest_props_handler
);
}
/**
@ -241,14 +268,6 @@ 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.
@ -260,89 +279,92 @@ function has_destroyed_component_ctx(current_value) {
* @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))}
*/
export function prop(props, key, flags, fallback) {
var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0;
var bindable = (flags & PROPS_IS_BINDABLE) !== 0;
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
var is_store_sub = false;
var prop_value;
if (bindable) {
[prop_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
} else {
prop_value = /** @type {V} */ (props[key]);
}
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
var setter =
(bindable &&
(get_descriptor(props, key)?.set ??
(is_entry_props && key in props && ((v) => (props[key] = v))))) ||
undefined;
var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true;
var fallback_used = false;
var get_fallback = () => {
fallback_used = true;
if (fallback_dirty) {
fallback_dirty = false;
if (lazy) {
fallback_value = untrack(/** @type {() => V} */ (fallback));
} else {
fallback_value = /** @type {V} */ (fallback);
}
fallback_value = lazy
? untrack(/** @type {() => V} */ (fallback))
: /** @type {V} */ (fallback);
}
return fallback_value;
};
if (prop_value === undefined && fallback !== undefined) {
if (setter && runes) {
e.props_invalid_value(key);
/** @type {((v: V) => void) | undefined} */
var setter;
if (bindable) {
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
setter =
get_descriptor(props, key)?.set ??
(is_entry_props && key in props ? (v) => (props[key] = v) : undefined);
}
var initial_value;
var is_store_sub = false;
if (bindable) {
[initial_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
} else {
initial_value = /** @type {V} */ (props[key]);
}
prop_value = get_fallback();
if (setter) setter(prop_value);
if (initial_value === undefined && fallback !== undefined) {
initial_value = get_fallback();
if (setter) {
if (runes) e.props_invalid_value(key);
setter(initial_value);
}
}
/** @type {() => V} */
var getter;
if (runes) {
getter = () => {
var value = /** @type {V} */ (props[key]);
if (value === undefined) return get_fallback();
fallback_dirty = true;
fallback_used = false;
return value;
};
} else {
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
// Replicate that behavior through using a derived
var derived_getter = (immutable ? derived : derived_safe_equal)(
() => /** @type {V} */ (props[key])
);
derived_getter.f |= LEGACY_DERIVED_PROP;
getter = () => {
var value = get(derived_getter);
if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
var value = /** @type {V} */ (props[key]);
if (value !== undefined) {
// in legacy mode, we don't revert to the fallback value
// if the prop goes from defined to undefined. The easiest
// way to model this is to make the fallback undefined
// as soon as the prop has a value
fallback_value = /** @type {V} */ (undefined);
}
return value === undefined ? fallback_value : value;
};
}
// easy mode — prop is never written to
if ((flags & PROPS_IS_UPDATED) === 0 && runes) {
// prop is never written to — we only need a getter
if (runes && (flags & PROPS_IS_UPDATED) === 0) {
return getter;
}
// intermediate mode — prop is written to, but the parent component had
// `bind:foo` which means we can just call `$$props.foo = value` directly
// prop is written to, but the parent component had `bind:foo` which
// means we can just call `$$props.foo = value` directly
if (setter) {
var legacy_parent = props.$$legacy;
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
@ -352,82 +374,53 @@ export function prop(props, key, flags, fallback) {
if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value);
}
return value;
} else {
return getter();
}
return getter();
};
}
// hard mode. this is where it gets ugly — the value in the child should
// synchronize with the parent, but it should also be possible to temporarily
// set the value to something else locally.
var from_child = false;
var was_from_child = false;
// The derived returns the current value. The underlying mutable
// source is written to from various places to persist this value.
var inner_current_value = mutable_source(prop_value);
var current_value = derived(() => {
var parent_value = getter();
var child_value = get(inner_current_value);
if (from_child) {
from_child = false;
was_from_child = true;
return child_value;
}
// Either prop is written to, but there's no binding, which means we
// create a derived that we can write to locally.
// Or we are in legacy mode where we always create a derived to replicate that
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
var overridden = false;
was_from_child = false;
return (inner_current_value.v = parent_value);
var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(() => {
overridden = false;
return getter();
});
// Ensure we eagerly capture the initial value if it's bindable
if (bindable) {
get(current_value);
}
// Capture the initial value if it's bindable
if (bindable) get(d);
if (!immutable) current_value.equals = safe_equals;
var parent_effect = /** @type {Effect} */ (active_effect);
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
// legacy nonsense — need to ensure the source is invalidated when necessary
// also needed for when handling inspect logic so we can inspect the correct source signal
if (captured_signals !== null) {
// set this so that we don't reset to the parent value if `d`
// is invalidated because of `invalidate_inner_signals` (rather
// than because the parent or child value changed)
from_child = was_from_child;
// invoke getters so that signals are picked up by `invalidate_inner_signals`
getter();
get(inner_current_value);
}
if (arguments.length > 0) {
const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value;
if (!current_value.equals(new_value)) {
from_child = true;
set(inner_current_value, new_value);
// To ensure the fallback value is consistent when used with proxies, we
// update the local fallback_value, but only if the fallback is actively used
if (fallback_used && fallback_value !== undefined) {
fallback_value = new_value;
}
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
if (has_destroyed_component_ctx(current_value)) {
return value;
}
set(d, new_value);
overridden = true;
untrack(() => get(current_value)); // force a synchronisation immediately
if (fallback_value !== undefined) {
fallback_value = new_value;
}
return value;
}
if (has_destroyed_component_ctx(current_value)) {
return current_value.v;
// special case — avoid recalculating the derived if we're in a
// teardown function and the prop was overridden locally, or the
// component was already destroyed (this latter part is necessary
// because `bind:this` can read props after the component has
// been destroyed. TODO simplify `bind:this`
if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
return d.v;
}
return get(current_value);
return get(d);
};
}

@ -11,7 +11,7 @@ import {
untrack,
increment_write_version,
update_effect,
reaction_sources,
current_sources,
check_dirtiness,
untracking,
is_destroying_effect,
@ -140,7 +140,7 @@ export function set(source, value, should_proxy = false) {
(!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) &&
is_runes() &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 &&
!(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction)
!current_sources?.includes(source)
) {
e.state_unsafe_mutation();
}
@ -259,6 +259,14 @@ export function update_pre(source, d = 1) {
return set(source, d === 1 ? ++value : --value);
}
/**
* Silently (without using `get`) increment a source
* @param {Source<number>} source
*/
export function increment(source) {
set(source, source.v + 1);
}
/**
* @param {Value} signal
* @param {number} status should be DIRTY or MAYBE_DIRTY

@ -1,4 +1,11 @@
import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client';
import type {
ComponentContext,
DevStackEntry,
Equals,
TemplateNode,
TransitionManager
} from '#client';
import type { Boundary } from '../dom/blocks/boundary';
export interface Signal {
/** Flags bitmask */
@ -40,6 +47,8 @@ export interface Reaction extends Signal {
fn: null | Function;
/** Signals that this signal reads from */
deps: null | Value[];
/** An AbortController that aborts when the signal is destroyed */
ac: null | AbortController;
}
export interface Derived<V = unknown> extends Value<V>, Reaction {
@ -76,8 +85,12 @@ export interface Effect extends Reaction {
last: null | Effect;
/** Parent effect */
parent: Effect | null;
/** The boundary this effect belongs to */
b: Boundary | null;
/** Dev only */
component_function?: any;
/** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */
dev_stack?: DevStackEntry | null;
}
export type Source<V = unknown> = Value<V>;

@ -20,13 +20,14 @@ import {
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
LEGACY_DERIVED_PROP,
DISCONNECTED,
EFFECT_IS_UPDATING
EFFECT_IS_UPDATING,
STALE_REACTION,
USER_EFFECT
} 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 { tracing_mode_flag } from '../flags/index.js';
@ -34,12 +35,14 @@ import { tracing_expressions, get_stack } from './dev/tracing.js';
import {
component_context,
dev_current_component_function,
dev_stack,
is_runes,
set_component_context,
set_dev_current_component_function
set_dev_current_component_function,
set_dev_stack
} from './context.js';
import { handle_error, invoke_error_boundary } from './error-handling.js';
import { snapshot } from '../shared/clone.js';
import { UNINITIALIZED } from '../../constants.js';
let is_flushing = false;
@ -85,17 +88,17 @@ export function set_active_effect(effect) {
/**
* When sources are created within a reaction, reading and writing
* them within that reaction should not cause a re-run
* @type {null | [active_reaction: Reaction, sources: Source[]]}
* @type {null | Source[]}
*/
export let reaction_sources = null;
export let current_sources = null;
/** @param {Value} value */
export function push_reaction_value(value) {
if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
if (reaction_sources === null) {
reaction_sources = [active_reaction, [value]];
if (current_sources === null) {
current_sources = [value];
} else {
reaction_sources[1].push(value);
current_sources.push(value);
}
}
}
@ -131,6 +134,13 @@ let write_version = 1;
/** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */
let read_version = 0;
export let update_version = read_version;
/** @param {number} value */
export function set_update_version(value) {
update_version = value;
}
// If we are working with a get() chain that has no active container,
// to prevent memory leaks, we skip adding the reaction.
export let skip_reaction = false;
@ -231,11 +241,13 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
var reactions = signal.reactions;
if (reactions === null) return;
if (current_sources?.includes(signal)) {
return;
}
for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i];
if (reaction_sources?.[1].includes(signal) && reaction_sources[0] === active_reaction) continue;
if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
} else if (effect === reaction) {
@ -256,9 +268,10 @@ export function update_reaction(reaction) {
var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction;
var previous_skip_reaction = skip_reaction;
var previous_reaction_sources = reaction_sources;
var previous_sources = current_sources;
var previous_component_context = component_context;
var previous_untracking = untracking;
var previous_update_version = update_version;
var flags = reaction.f;
@ -269,13 +282,18 @@ 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;
reaction_sources = null;
current_sources = null;
set_component_context(reaction.ctx);
untracking = false;
read_version++;
update_version = ++read_version;
reaction.f |= EFFECT_IS_UPDATING;
if (reaction.ac !== null) {
reaction.ac.abort(STALE_REACTION);
reaction.ac = null;
}
try {
var result = /** @type {Function} */ (0, reaction.fn)();
var deps = reaction.deps;
@ -294,7 +312,12 @@ export function update_reaction(reaction) {
reaction.deps = deps = new_deps;
}
if (!skip_reaction) {
if (
!skip_reaction ||
// Deriveds that already have reactions can cleanup, so we still add them as reactions
((flags & DERIVED) !== 0 &&
/** @type {import('#client').Derived} */ (reaction).reactions !== null)
) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
@ -347,9 +370,10 @@ export function update_reaction(reaction) {
untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction;
reaction_sources = previous_reaction_sources;
current_sources = previous_sources;
set_component_context(previous_component_context);
untracking = previous_untracking;
update_version = previous_update_version;
reaction.f ^= EFFECT_IS_UPDATING;
}
@ -434,6 +458,9 @@ export function update_effect(effect) {
if (DEV) {
var previous_component_fn = dev_current_component_function;
set_dev_current_component_function(effect.component_function);
var previous_stack = /** @type {any} */ (dev_stack);
// only block effects have a dev stack, keep the current one otherwise
set_dev_stack(effect.dev_stack ?? dev_stack);
}
try {
@ -468,6 +495,7 @@ export function update_effect(effect) {
if (DEV) {
set_dev_current_component_function(previous_component_fn);
set_dev_stack(previous_stack);
}
}
}
@ -561,6 +589,8 @@ function flush_queued_effects(effects) {
if ((effect.f & (DESTROYED | INERT)) === 0) {
if (check_dirtiness(effect)) {
var wv = write_version;
update_effect(effect);
// Effects with no dependencies or teardown do not get added to the effect tree.
@ -577,9 +607,19 @@ function flush_queued_effects(effects) {
effect.fn = null;
}
}
// if state is written in a user effect, abort and re-schedule, lest we run
// effects that should be removed as a result of the state change
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
break;
}
}
}
}
for (; i < length; i += 1) {
schedule_effect(effects[i]);
}
}
/**
@ -724,7 +764,7 @@ export function get(signal) {
// Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) {
if (!reaction_sources?.[1].includes(signal) || reaction_sources[0] !== active_reaction) {
if (!current_sources?.includes(signal)) {
var deps = active_reaction.deps;
if (signal.rv < read_version) {
signal.rv = read_version;
@ -759,7 +799,7 @@ export function get(signal) {
}
}
if (is_derived) {
if (is_derived && !is_destroying_effect) {
derived = /** @type {Derived} */ (signal);
if (check_dirtiness(derived)) {
@ -800,13 +840,48 @@ export function get(signal) {
}
}
if (is_destroying_effect && old_values.has(signal)) {
if (is_destroying_effect) {
if (old_values.has(signal)) {
return old_values.get(signal);
}
if (is_derived) {
derived = /** @type {Derived} */ (signal);
var value = derived.v;
// if the derived is dirty, or depends on the values that just changed, re-execute
if ((derived.f & CLEAN) !== 0 || depends_on_old_values(derived)) {
value = execute_derived(derived);
}
old_values.set(derived, value);
return value;
}
}
return signal.v;
}
/** @param {Derived} derived */
function depends_on_old_values(derived) {
if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst
if (derived.deps === null) return false;
for (const dep of derived.deps) {
if (old_values.has(dep)) {
return true;
}
if ((dep.f & DERIVED) !== 0 && depends_on_old_values(/** @type {Derived} */ (dep))) {
return true;
}
}
return false;
}
/**
* Like `get`, but checks for `undefined`. Used for `var` declarations because they can be accessed before being declared
* @template V
@ -852,18 +927,8 @@ export function invalidate_inner_signals(fn) {
var captured = capture_signals(() => untrack(fn));
for (var signal of captured) {
// Go one level up because derived signals created as part of props in legacy mode
if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
for (const dep of /** @type {Derived} */ (signal).deps || []) {
if ((dep.f & DERIVED) === 0) {
// Use internal_set instead of set here and below to avoid mutation validation
internal_set(dep, dep.v);
}
}
} else {
internal_set(signal, signal.v);
}
}
}
/**

@ -14,16 +14,8 @@ export type ComponentContext = {
p: null | ComponentContext;
/** context */
c: null | Map<unknown, unknown>;
/** destroyed */
d: boolean;
/** deferred effects */
e: null | Array<{
fn: () => void | (() => void);
effect: null | Effect;
reaction: null | Reaction;
}>;
/** mounted */
m: boolean;
e: null | Array<() => void | (() => void)>;
/**
* props needed for legacy mode lifecycle functions, and for `createEventDispatcher`
* @deprecated remove in 6.0
@ -187,4 +179,13 @@ export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];
export interface DevStackEntry {
file: string;
type: 'component' | 'if' | 'each' | 'await' | 'key' | 'render';
line: number;
column: number;
parent: DevStackEntry | null;
componentTag?: string;
}
export * from './reactivity/types';

@ -25,7 +25,13 @@ export function assignment_value_stale(property, location) {
*/
export function binding_property_non_reactive(binding, location) {
if (DEV) {
console.warn(`%c[svelte] binding_property_non_reactive\n%c${location ? `\`${binding}\` (${location}) is binding to a non-reactive property` : `\`${binding}\` is binding to a non-reactive property`}\nhttps://svelte.dev/e/binding_property_non_reactive`, bold, normal);
console.warn(
`%c[svelte] binding_property_non_reactive\n%c${location
? `\`${binding}\` (${location}) is binding to a non-reactive property`
: `\`${binding}\` is binding to a non-reactive property`}\nhttps://svelte.dev/e/binding_property_non_reactive`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/binding_property_non_reactive`);
}
@ -76,7 +82,13 @@ export function hydration_attribute_changed(attribute, html, value) {
*/
export function hydration_html_changed(location) {
if (DEV) {
console.warn(`%c[svelte] hydration_html_changed\n%c${location ? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value` : 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'}\nhttps://svelte.dev/e/hydration_html_changed`, bold, normal);
console.warn(
`%c[svelte] hydration_html_changed\n%c${location
? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value`
: 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'}\nhttps://svelte.dev/e/hydration_html_changed`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/hydration_html_changed`);
}
@ -88,7 +100,13 @@ export function hydration_html_changed(location) {
*/
export function hydration_mismatch(location) {
if (DEV) {
console.warn(`%c[svelte] hydration_mismatch\n%c${location ? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}` : 'Hydration failed because the initial UI does not match what was rendered on the server'}\nhttps://svelte.dev/e/hydration_mismatch`, bold, normal);
console.warn(
`%c[svelte] hydration_mismatch\n%c${location
? `Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near ${location}`
: 'Hydration failed because the initial UI does not match what was rendered on the server'}\nhttps://svelte.dev/e/hydration_mismatch`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/hydration_mismatch`);
}

@ -0,0 +1,13 @@
import { STALE_REACTION } from '#client/constants';
/** @type {AbortController | null} */
export let controller = null;
export function abort() {
controller?.abort(STALE_REACTION);
controller = null;
}
export function getAbortSignal() {
return (controller ??= new AbortController()).signal;
}

@ -1,5 +1,7 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
/**
* `%name%(...)` is not available on the server
* @param {string} name
@ -9,5 +11,6 @@ export function lifecycle_function_unavailable(name) {
const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server\nhttps://svelte.dev/e/lifecycle_function_unavailable`);
error.name = 'Svelte error';
throw error;
}

@ -18,6 +18,7 @@ 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';
import { abort } from './abort-signal.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
@ -66,6 +67,7 @@ export let on_destroy = [];
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
try {
const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
const prev_on_destroy = on_destroy;
@ -110,6 +112,9 @@ export function render(component, options = {}) {
html: payload.out,
body: payload.out
};
} finally {
abort();
}
}
/**

@ -11,6 +11,7 @@ export function invalid_default_snippet() {
const error = new Error(`invalid_default_snippet\nCannot use \`{@render children(...)}\` if the parent component uses \`let:\` directives. Consider using a named snippet instead\nhttps://svelte.dev/e/invalid_default_snippet`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_default_snippet`);
@ -26,6 +27,7 @@ export function invalid_snippet_arguments() {
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`);
@ -42,6 +44,7 @@ export function lifecycle_outside_component(name) {
const error = new Error(`lifecycle_outside_component\n\`${name}(...)\` can only be used during component initialisation\nhttps://svelte.dev/e/lifecycle_outside_component`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/lifecycle_outside_component`);
@ -57,6 +60,7 @@ export function snippet_without_render_tag() {
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`);
@ -73,6 +77,7 @@ export function store_invalid_shape(name) {
const error = new Error(`store_invalid_shape\n\`${name}\` is not a store with a \`subscribe\` method\nhttps://svelte.dev/e/store_invalid_shape`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/store_invalid_shape`);
@ -88,6 +93,7 @@ export function svelte_element_invalid_this_value() {
const error = new Error(`svelte_element_invalid_this_value\nThe \`this\` prop on \`<svelte:element>\` must be a string, if defined\nhttps://svelte.dev/e/svelte_element_invalid_this_value`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`);

@ -25,11 +25,15 @@ export function dynamic_void_element_content(tag) {
*/
export function state_snapshot_uncloneable(properties) {
if (DEV) {
console.warn(`%c[svelte] state_snapshot_uncloneable\n%c${properties
console.warn(
`%c[svelte] state_snapshot_uncloneable\n%c${properties
? `The following properties cannot be cloned with \`$state.snapshot\` — the return value contains the originals:
${properties}`
: 'Value cannot be cloned with `$state.snapshot` — the original value was returned'}\nhttps://svelte.dev/e/state_snapshot_uncloneable`, bold, normal);
: 'Value cannot be cloned with `$state.snapshot` — the original value was returned'}\nhttps://svelte.dev/e/state_snapshot_uncloneable`,
bold,
normal
);
} else {
console.warn(`https://svelte.dev/e/state_snapshot_uncloneable`);
}

@ -5,7 +5,7 @@ import { writable } from '../store/shared/index.js';
import { loop } from '../internal/client/loop.js';
import { raf } from '../internal/client/timing.js';
import { is_date } from './utils.js';
import { set, source } from '../internal/client/reactivity/sources.js';
import { set, state } from '../internal/client/reactivity/sources.js';
import { render_effect } from '../internal/client/reactivity/effects.js';
import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
@ -170,9 +170,9 @@ export function spring(value, opts = {}) {
* @since 5.8.0
*/
export class Spring {
#stiffness = source(0.15);
#damping = source(0.8);
#precision = source(0.01);
#stiffness = state(0.15);
#damping = state(0.8);
#precision = state(0.01);
#current;
#target;
@ -194,8 +194,8 @@ export class Spring {
* @param {SpringOpts} [options]
*/
constructor(value, options = {}) {
this.#current = DEV ? tag(source(value), 'Spring.current') : source(value);
this.#target = DEV ? tag(source(value), 'Spring.target') : source(value);
this.#current = DEV ? tag(state(value), 'Spring.current') : state(value);
this.#target = DEV ? tag(state(value), 'Spring.target') : state(value);
if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1);
if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1);

@ -6,7 +6,7 @@ import { raf } from '../internal/client/timing.js';
import { loop } from '../internal/client/loop.js';
import { linear } from '../easing/index.js';
import { is_date } from './utils.js';
import { set, source } from '../internal/client/reactivity/sources.js';
import { set, state } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { get, render_effect } from 'svelte/internal/client';
import { DEV } from 'esm-env';
@ -191,8 +191,8 @@ export class Tween {
* @param {TweenedOptions<T>} options
*/
constructor(value, options = {}) {
this.#current = source(value);
this.#target = source(value);
this.#current = state(value);
this.#target = state(value);
this.#defaults = options;
if (DEV) {

@ -1,8 +1,7 @@
import { get, tick, untrack } from '../internal/client/runtime.js';
import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
import { source } from '../internal/client/reactivity/sources.js';
import { source, increment } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { increment } from './utils.js';
import { DEV } from 'esm-env';
/**

@ -1,9 +1,8 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { set, source, state } from '../internal/client/reactivity/sources.js';
import { set, source, state, increment } from '../internal/client/reactivity/sources.js';
import { label, tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
import { get, update_version } from '../internal/client/runtime.js';
/**
* A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object.
@ -56,6 +55,7 @@ export class SvelteMap extends Map {
#sources = new Map();
#version = state(0);
#size = state(0);
#update_version = update_version || -1;
/**
* @param {Iterable<readonly [K, V]> | null | undefined} [value]
@ -79,6 +79,19 @@ export class SvelteMap extends Map {
}
}
/**
* If the source is being created inside the same reaction as the SvelteMap instance,
* we use `state` so that it will not be a dependency of the reaction. Otherwise we
* use `source` so it will be.
*
* @template T
* @param {T} value
* @returns {Source<T>}
*/
#source(value) {
return update_version === this.#update_version ? state(value) : source(value);
}
/** @param {K} key */
has(key) {
var sources = this.#sources;
@ -87,7 +100,7 @@ export class SvelteMap extends Map {
if (s === undefined) {
var ret = super.get(key);
if (ret !== undefined) {
s = source(0);
s = this.#source(0);
if (DEV) {
tag(s, `SvelteMap get(${label(key)})`);
@ -123,7 +136,7 @@ export class SvelteMap extends Map {
if (s === undefined) {
var ret = super.get(key);
if (ret !== undefined) {
s = source(0);
s = this.#source(0);
if (DEV) {
tag(s, `SvelteMap get(${label(key)})`);
@ -154,7 +167,7 @@ export class SvelteMap extends Map {
var version = this.#version;
if (s === undefined) {
s = source(0);
s = this.#source(0);
if (DEV) {
tag(s, `SvelteMap get(${label(key)})`);
@ -219,8 +232,7 @@ export class SvelteMap extends Map {
if (this.#size.v !== sources.size) {
for (var key of super.keys()) {
if (!sources.has(key)) {
var s = source(0);
var s = this.#source(0);
if (DEV) {
tag(s, `SvelteMap get(${label(key)})`);
}

@ -1,9 +1,8 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { source, set, state } from '../internal/client/reactivity/sources.js';
import { source, set, state, increment } from '../internal/client/reactivity/sources.js';
import { label, tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
import { get, update_version } from '../internal/client/runtime.js';
var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
@ -50,6 +49,7 @@ export class SvelteSet extends Set {
#sources = new Map();
#version = state(0);
#size = state(0);
#update_version = update_version || -1;
/**
* @param {Iterable<T> | null | undefined} [value]
@ -75,6 +75,19 @@ export class SvelteSet extends Set {
if (!inited) this.#init();
}
/**
* If the source is being created inside the same reaction as the SvelteSet instance,
* we use `state` so that it will not be a dependency of the reaction. Otherwise we
* use `source` so it will be.
*
* @template T
* @param {T} value
* @returns {Source<T>}
*/
#source(value) {
return update_version === this.#update_version ? state(value) : source(value);
}
// We init as part of the first instance so that we can treeshake this class
#init() {
inited = true;
@ -116,7 +129,7 @@ export class SvelteSet extends Set {
return false;
}
s = source(true);
s = this.#source(true);
if (DEV) {
tag(s, `SvelteSet has(${label(value)})`);

@ -1,9 +1,8 @@
import { DEV } from 'esm-env';
import { state } from '../internal/client/reactivity/sources.js';
import { state, increment } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { get_current_url } from './url.js';
import { increment } from './utils.js';
export const REPLACE = Symbol();

@ -1,7 +0,0 @@
/** @import { Source } from '#client' */
import { set } from '../internal/client/reactivity/sources.js';
/** @param {Source<number>} source */
export function increment(source) {
set(source, source.v + 1);
}

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.34.8';
export const VERSION = '5.35.6';
export const PUBLIC_VERSION = '5';

@ -21,6 +21,8 @@ const { test, run } = suite<ParserTest>(async (config, cwd) => {
)
);
delete actual.comments;
// run `UPDATE_SNAPSHOTS=true pnpm test parser` to update parser tests
if (process.env.UPDATE_SNAPSHOTS) {
fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t'));

@ -2,6 +2,7 @@ import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
accessors: false,
test({ assert, logs, target }) {
assert.deepEqual(logs, ['primitive', 'object']);
target.querySelector('button')?.click();

@ -0,0 +1,22 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, logs, target }) {
const [increment] = target.querySelectorAll('button');
flushSync(() => increment.click());
flushSync(() => increment.click());
flushSync(() => increment.click());
assert.deepEqual(logs, [
'count: 1',
'squared: 1',
'count: 2',
'squared: 4',
'count: 3',
'squared: 9',
'count: 4'
]);
}
});

@ -0,0 +1,20 @@
<script>
let count = $state(1);
let squared = $derived(count * count);
$effect(() => {
console.log(`count: ${count}`);
return () => {
console.log(`squared: ${squared}`);
};
});
</script>
<button onclick={() => count++}>increment</button>
<p>count: {count}</p>
{#if count % 2 === 0}
<p id="squared">squared: {squared}</p>
{/if}

@ -0,0 +1,11 @@
<script>
import B from './B.svelte';
let { boolean, closed } = $props();
$effect(() => {
console.log(boolean);
});
</script>
<B {closed} />

@ -0,0 +1,9 @@
<script>
import { close } from './Child.svelte';
let { closed } = $props();
$effect(() => {
if (closed) close();
});
</script>

@ -0,0 +1,20 @@
<script module>
let object = $state();
export function open() {
object = { boolean: true };
}
export function close() {
object = undefined;
}
</script>
<script>
let { children } = $props();
</script>
{#if object?.boolean}
<!-- error occurs here, this is executed when the if should already make it falsy -->
{@render children(object.boolean)}
{/if}

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [open, close] = target.querySelectorAll('button');
flushSync(() => open.click());
flushSync(() => close.click());
assert.deepEqual(logs, [true]);
}
});

@ -0,0 +1,23 @@
<script>
import A from './A.svelte';
import Child, { open } from './Child.svelte';
let closed = $state(false);
</script>
<button onclick={open}>
open
</button>
<button onclick={() => closed = true}>
close
</button>
<hr>
<Child>
{#snippet children(boolean)}
<A {closed} {boolean} />
{/snippet}
</Child>

@ -0,0 +1,9 @@
<script>
import B from './B.svelte';
let { boolean, closed } = $props();
</script>
<span>{boolean}</span>
<B {closed} />

@ -0,0 +1,9 @@
<script>
import { close } from './Child.svelte';
let { closed } = $props();
$effect.pre(() => {
if (closed) close();
});
</script>

@ -0,0 +1,20 @@
<script module>
let object = $state();
export function open() {
object = { nested: { boolean: true } };
}
export function close() {
object = undefined;
}
</script>
<script>
let { children } = $props();
</script>
{#if object?.nested}
<!-- error occurs here, this is executed when the if should already make it falsy -->
{@render children(object.nested)}
{/if}

@ -0,0 +1,15 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
skip: true,
async test({ assert, target, logs }) {
const [open, close] = target.querySelectorAll('button');
flushSync(() => open.click());
flushSync(() => close.click());
assert.deepEqual(logs, [true]);
}
});

@ -0,0 +1,22 @@
<script>
import A from './A.svelte';
import Child, { open } from './Child.svelte';
let closed = $state(false);
</script>
<button onclick={open}>
open
</button>
<button onclick={() => closed = true}>
close
</button>
<hr>
<Child>
{#snippet children(nested)}
<A {closed} boolean={nested.boolean} />
{/snippet}
</Child>

@ -0,0 +1,15 @@
<script>
let { message, count } = $props();
$effect(() => () => {
console.log(count, message);
});
</script>
<p>{count}</p>
<!-- we need these so that the props are made into deriveds -->
<button disabled onclick={() => {
count += 1;
message += '!';
}}>update</button>

@ -0,0 +1,17 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
async test({ assert, target, logs }) {
const [increment, toggle] = target.querySelectorAll('button');
flushSync(() => toggle.click());
assert.deepEqual(logs, [0, 'hello']);
flushSync(() => toggle.click());
flushSync(() => increment.click());
flushSync(() => increment.click());
assert.deepEqual(logs, [0, 'hello', 1, 'hello']);
}
});

@ -0,0 +1,13 @@
<script>
import Component from "./Component.svelte";
let message = $state('hello');
let count = $state(0);
</script>
<button onclick={() => count++}>{count}</button>
<button onclick={() => message = message === 'hello' ? 'goodbye' : 'hello'}>{message}</button>
{#if count < 2 && message === 'hello'}
<Component {count} {message} />
{/if}

@ -0,0 +1,34 @@
import { test } from '../../test';
export default test({
html: `<button>increment</button><p>loading...</p>`,
async test({ assert, target, variant, logs }) {
await new Promise((f) => setTimeout(f, 50));
if (variant === 'hydrate') {
assert.deepEqual(logs, [
'aborted',
'StaleReactionError',
'The reaction that called `getAbortSignal()` was re-run or destroyed'
]);
}
logs.length = 0;
const [button] = target.querySelectorAll('button');
await new Promise((f) => setTimeout(f, 50));
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>0</p>');
button.click();
await new Promise((f) => setTimeout(f, 50));
assert.htmlEqual(target.innerHTML, '<button>increment</button><p>2</p>');
assert.deepEqual(logs, [
'aborted',
'StaleReactionError',
'The reaction that called `getAbortSignal()` was re-run or destroyed'
]);
}
});

@ -0,0 +1,33 @@
<script>
import { getAbortSignal } from 'svelte';
let count = $state(0);
let delayed_count = $derived.by(async () => {
let c = count;
const signal = getAbortSignal();
await new Promise((f) => setTimeout(f));
if (signal.aborted) {
console.log('aborted', signal.reason.name, signal.reason.message);
}
return c;
});
</script>
<button onclick={async () => {
count += 1;
await Promise.resolve();
count += 1;
}}>increment</button>
{#await delayed_count}
<p>loading...</p>
{:then count}
<p>{count}</p>
{:catch error}
{console.log('this should never be rendered')}
{/await}

@ -0,0 +1,9 @@
import { ok, test } from '../../test';
export default test({
async test({ assert, target, instance }) {
const select = target.querySelector('select');
ok(select);
assert.equal(select.selectedIndex, 1);
}
});

@ -0,0 +1,8 @@
<script>
let others = {onclick: ()=> {}}
</script>
<select {...others}>
<option>o1</option>
<option selected>o2</option>
</select>

@ -8,7 +8,8 @@ export default test({
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
const [button1, button2, button3, button4, button5, button6, button7, button8] =
target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
@ -19,5 +20,35 @@ export default test({
button2?.click();
flushSync();
});
assert.throws(() => {
button3?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button4?.click();
flushSync();
});
assert.throws(() => {
button5?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button6?.click();
flushSync();
});
assert.throws(() => {
button7?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button8?.click();
flushSync();
});
}
});

@ -1,27 +1,101 @@
<script>
import { SvelteMap } from 'svelte/reactivity';
let visibleExternal = $state(false);
let external = new SvelteMap();
const throws = $derived.by(() => {
external.set(1, 1);
return external;
let outside_basic = $state(false);
let outside_basic_map = new SvelteMap();
const throw_basic = $derived.by(() => {
outside_basic_map.set(1, 1);
return outside_basic_map;
});
let visibleInternal = $state(false);
const works = $derived.by(() => {
let internal = new SvelteMap();
internal.set(1, 1);
return internal;
let inside_basic = $state(false);
const works_basic = $derived.by(() => {
let inside = new SvelteMap();
inside.set(1, 1);
return inside;
});
let outside_has = $state(false);
let outside_has_map = new SvelteMap([[1, 1]]);
const throw_has = $derived.by(() => {
outside_has_map.has(1);
outside_has_map.set(1, 2);
return outside_has_map;
});
let inside_has = $state(false);
const works_has = $derived.by(() => {
let inside = new SvelteMap([[1, 1]]);
inside.has(1);
inside.set(1, 1);
return inside;
});
let outside_get = $state(false);
let outside_get_map = new SvelteMap([[1, 1]]);
const throw_get = $derived.by(() => {
outside_get_map.get(1);
outside_get_map.set(1, 2);
return outside_get_map;
});
let inside_get = $state(false);
const works_get = $derived.by(() => {
let inside = new SvelteMap([[1, 1]]);
inside.get(1);
inside.set(1, 1);
return inside;
});
let outside_values = $state(false);
let outside_values_map = new SvelteMap([[1, 1]]);
const throw_values = $derived.by(() => {
outside_values_map.values(1);
outside_values_map.set(1, 2);
return outside_values_map;
});
let inside_values = $state(false);
const works_values = $derived.by(() => {
let inside = new SvelteMap([[1, 1]]);
inside.values();
inside.set(1, 1);
return inside;
});
</script>
<button onclick={() => (visibleExternal = true)}>external</button>
{#if visibleExternal}
{throws}
<button onclick={() => (outside_basic = true)}>external</button>
{#if outside_basic}
{throw_basic}
{/if}
<button onclick={() => (inside_basic = true)}>internal</button>
{#if inside_basic}
{works_basic}
{/if}
<button onclick={() => (outside_has = true)}>external</button>
{#if outside_has}
{throw_has}
{/if}
<button onclick={() => (visibleInternal = true)}>internal</button>
{#if visibleInternal}
{works}
<button onclick={() => (inside_has = true)}>internal</button>
{#if inside_has}
{works_has}
{/if}
<button onclick={() => (outside_get = true)}>external</button>
{#if outside_get}
{throw_get}
{/if}
<button onclick={() => (inside_get = true)}>internal</button>
{#if inside_get}
{works_get}
{/if}
<button onclick={() => (outside_values = true)}>external</button>
{#if outside_values}
{throw_values}
{/if}
<button onclick={() => (inside_values = true)}>internal</button>
{#if inside_values}
{works_values}
{/if}

@ -8,7 +8,7 @@ export default test({
},
test({ assert, target }) {
const [button1, button2] = target.querySelectorAll('button');
const [button1, button2, button3, button4] = target.querySelectorAll('button');
assert.throws(() => {
button1?.click();
@ -19,5 +19,15 @@ export default test({
button2?.click();
flushSync();
});
assert.throws(() => {
button3?.click();
flushSync();
}, /state_unsafe_mutation/);
assert.doesNotThrow(() => {
button4?.click();
flushSync();
});
}
});

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

Loading…
Cancel
Save