Merge remote-tracking branch 'origin/main' into svelte-custom-renderer

svelte-custom-renderer
paoloricciuti 2 days ago
commit c82987e0f1

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure proper HMR updates for dynamic components

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: invalidate `@const` tags based on visible references in legacy mode

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: handle parens in template expressions more robustly

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: disallow `--` in `idPrefix`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: correct types for `ontoggle` on `<details>` elements

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't override `$destroy/set/on` instance methods in dev mode

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: unskip branches of earlier batches after commit

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: never set derived.v inside fork

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: skip rebase logic in non-async mode

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't reset status of uninitialized deriveds

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: defer error boundary rendering in forks

@ -1,5 +1,27 @@
# svelte
## 5.55.2
### Patch Changes
- fix: invalidate `@const` tags based on visible references in legacy mode ([#18041](https://github.com/sveltejs/svelte/pull/18041))
- fix: handle parens in template expressions more robustly ([#18075](https://github.com/sveltejs/svelte/pull/18075))
- fix: disallow `--` in `idPrefix` ([#18038](https://github.com/sveltejs/svelte/pull/18038))
- fix: correct types for `ontoggle` on `<details>` elements ([#18063](https://github.com/sveltejs/svelte/pull/18063))
- fix: don't override `$destroy/set/on` instance methods in dev mode ([#18034](https://github.com/sveltejs/svelte/pull/18034))
- fix: unskip branches of earlier batches after commit ([#18048](https://github.com/sveltejs/svelte/pull/18048))
- fix: never set derived.v inside fork ([#18037](https://github.com/sveltejs/svelte/pull/18037))
- fix: skip rebase logic in non-async mode ([#18040](https://github.com/sveltejs/svelte/pull/18040))
- fix: don't reset status of uninitialized deriveds ([#18054](https://github.com/sveltejs/svelte/pull/18054))
## 5.55.1
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.55.1",
"version": "5.55.2",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -4,8 +4,10 @@
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import * as e from '../../errors.js';
const ParserWithTS = acorn.Parser.extend(tsPlugin());
const JSParser = acorn.Parser;
const TSParser = JSParser.extend(tsPlugin());
/**
* @typedef {Comment & {
@ -21,15 +23,15 @@ const ParserWithTS = acorn.Parser.extend(tsPlugin());
* @param {boolean} [is_script]
*/
export function parse(source, comments, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const acorn = typescript ? TSParser : JSParser;
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
// @ts-expect-error
const parse_statement = acorn.prototype.parseStatement;
// If we're dealing with a <script> then it might contain an export
// for something that doesn't exist directly inside but is inside the
@ -37,7 +39,7 @@ export function parse(source, comments, typescript, is_script) {
// an error in these cases
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = function (...args) {
acorn.prototype.parseStatement = function (...args) {
const v = parse_statement.call(this, ...args);
// @ts-ignore
this.undefinedExports = {};
@ -45,25 +47,27 @@ export function parse(source, comments, typescript, is_script) {
};
}
let ast;
try {
ast = parser.parse(source, {
const ast = acorn.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
return /** @type {Program} */ (ast);
} catch (err) {
// TODO the `return` in necessary for TS<7 due to a bug; otherwise
// the `finally` block is regarded as unreachable
return handle_parse_error(err);
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
// @ts-expect-error
acorn.prototype.parseStatement = parse_statement;
}
}
add_comments(ast);
return /** @type {Program} */ (ast);
}
/**
@ -73,21 +77,35 @@ export function parse(source, comments, typescript, is_script) {
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(parser, source, index) {
const _ = parser.ts ? ParserWithTS : acorn.Parser;
const acorn = parser.ts ? TSParser : JSParser;
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
const ast = _.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true,
preserveParens: true
});
try {
const ast = acorn.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true,
preserveParens: true
});
add_comments(ast);
add_comments(ast);
return ast;
return ast;
} catch (e) {
handle_parse_error(e);
}
}
const regex_position_indicator = / \(\d+:\d+\)$/;
/**
* @param {any} err
* @returns {never}
*/
function handle_parse_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/**

@ -11,8 +11,6 @@ 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+\)$/;
/** @param {number} cc */
function is_whitespace(cc) {
// fast path for common whitespace
@ -175,14 +173,6 @@ export class Parser {
return this.stack[this.stack.length - 1];
}
/**
* @param {any} err
* @returns {never}
*/
acorn_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/**
* @param {string} str
* @param {boolean} required

@ -35,36 +35,32 @@ export default function read_pattern(parser) {
const pattern_string = parser.template.slice(start, i);
try {
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template
.slice(0, start)
.replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
/** @type {any} */
let expression = remove_parens(
parse_expression_at(parser, `${space_with_newline}(${pattern_string} = 1)`, start - 1)
);
expression = expression.left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end;
}
return expression;
} catch (error) {
parser.acorn_error(error);
// the length of the `space_with_newline` has to be start - 1
// because we added a `(` in front of the pattern_string,
// which shifted the entire string to right by 1
// so we offset it by removing 1 character in the `space_with_newline`
// to achieve that, we remove the 1st space encountered,
// so it will not affect the `column` of the node
let space_with_newline = parser.template
.slice(0, start)
.replace(regex_not_newline_characters, ' ');
const first_space = space_with_newline.indexOf(' ');
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
/** @type {any} */
let expression = remove_parens(
parse_expression_at(parser, `${space_with_newline}(${pattern_string} = 1)`, start - 1)
);
expression = expression.left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end;
}
return expression;
}
/**

@ -54,6 +54,6 @@ export default function read_expression(parser, opening_token, disallow_loose) {
}
}
parser.acorn_error(err);
throw err;
}
}

@ -31,14 +31,7 @@ export function read_script(parser, start, attributes) {
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_starts_with_closing_script_tag);
/** @type {Program} */
let ast;
try {
ast = acorn.parse(source, parser.root.comments, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}
const ast = acorn.parse(source, parser.root.comments, parser.ts, true);
ast.start = script_start;

@ -62,6 +62,8 @@ 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');
/** An anchor might change, via this symbol on the original anchor we can tell HMR about the updated anchor */
export const HMR_ANCHOR = Symbol('hmr anchor');
/** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */
export const STALE_REACTION = new (class StaleReactionError extends Error {

@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode } from '#client' */
import { FILENAME, HMR } from '../../../constants.js';
import { EFFECT_TRANSPARENT } from '#client/constants';
import { EFFECT_TRANSPARENT, HMR_ANCHOR } from '#client/constants';
import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set, source } from '../reactivity/sources.js';
@ -15,10 +15,10 @@ export function hmr(fn) {
const current = source(fn);
/**
* @param {TemplateNode} anchor
* @param {TemplateNode} initial_anchor
* @param {any} props
*/
function wrapper(anchor, props) {
function wrapper(initial_anchor, props) {
let component = {};
let instance = {};
@ -26,6 +26,7 @@ export function hmr(fn) {
let effect;
let ran = false;
let anchor = initial_anchor;
block(() => {
if (component === (component = get(current))) {
@ -39,6 +40,8 @@ export function hmr(fn) {
}
effect = branch(() => {
anchor = /** @type {any} */ (anchor)[HMR_ANCHOR] ?? anchor;
// when the component is invalidated, replace it without transitions
if (ran) set_should_intro(false);

@ -35,7 +35,7 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, current_batch, schedule_effect } from '../../reactivity/batch.js';
import { Batch, current_batch, previous_batch, schedule_effect } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
@ -406,15 +406,29 @@ export class Boundary {
/** @param {unknown} error */
error(error) {
var onerror = this.#props.onerror;
let failed = this.#props.failed;
// 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) {
if (!this.#props.onerror && !this.#props.failed) {
throw error;
}
if (current_batch?.is_fork) {
if (this.#main_effect) current_batch.skip_effect(this.#main_effect);
if (this.#pending_effect) current_batch.skip_effect(this.#pending_effect);
if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
current_batch.on_fork_commit(() => {
this.#handle_error(error);
});
} else {
this.#handle_error(error);
}
}
/**
* @param {unknown} error
*/
#handle_error(error) {
if (this.#main_effect) {
destroy_effect(this.#main_effect);
this.#main_effect = null;
@ -436,6 +450,8 @@ export class Boundary {
set_hydrate_node(skip_nodes());
}
var onerror = this.#props.onerror;
let failed = this.#props.failed;
var did_reset = false;
var calling_on_error = false;

@ -8,6 +8,7 @@ import {
pause_effect,
resume_effect
} from '../../reactivity/effects.js';
import { HMR_ANCHOR } from '../../constants.js';
import { hydrate_node, hydrating } from '../hydration.js';
import {
create_text,
@ -18,6 +19,7 @@ import {
remove_node,
get_last_child
} from '../operations.js';
import { DEV } from 'esm-env';
import { push_renderer, renderer as current_renderer } from '../../custom-renderer/state.js';
/**
@ -112,6 +114,12 @@ export class BranchManager {
this.#onscreen.set(key, offscreen.effect);
this.#offscreen.delete(key);
if (DEV) {
// Tell hmr.js about the anchor it should use for updates,
// since the initial one will be removed
/** @type {any} */ (offscreen.fragment.lastChild)[HMR_ANCHOR] = this.anchor;
}
// remove the anchor...
remove_node(/** @type {ChildNode} */ (get_last_child(offscreen.fragment)));

@ -270,6 +270,8 @@ export function run(thunks) {
for (const fn of thunks.slice(1)) {
promise = promise
.then(() => {
restore();
if (errored) {
throw errored.error;
}
@ -278,7 +280,6 @@ export function run(thunks) {
throw STALE_REACTION;
}
restore();
return fn();
})
.catch(handle_error);

@ -120,6 +120,12 @@ export class Batch {
*/
#discard_callbacks = new Set();
/**
* Callbacks that should run only when a fork is committed.
* @type {Set<(batch: Batch) => void>}
*/
#fork_commit_callbacks = new Set();
/**
* Async effects that are currently in flight
* @type {Map<Effect, number>}
@ -489,6 +495,7 @@ export class Batch {
discard() {
for (const fn of this.#discard_callbacks) fn(this);
this.#discard_callbacks.clear();
this.#fork_commit_callbacks.clear();
batches.delete(this);
}
@ -686,6 +693,16 @@ export class Batch {
this.#discard_callbacks.add(fn);
}
/** @param {(batch: Batch) => void} fn */
on_fork_commit(fn) {
this.#fork_commit_callbacks.add(fn);
}
run_fork_commit_callbacks() {
for (const fn of this.#fork_commit_callbacks) fn(this);
this.#fork_commit_callbacks.clear();
}
settled() {
return (this.#deferred ??= deferred()).promise;
}
@ -1212,6 +1229,10 @@ export function fork(fn) {
source.wv = increment_write_version();
}
batch.activate();
batch.run_fork_commit_callbacks();
batch.deactivate();
// trigger any `$state.eager(...)` expressions with the new state.
// eager effects don't get scheduled like other effects, so we
// can't just encounter them during traversal, we need to

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

@ -0,0 +1,33 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [show, commit] = target.querySelectorAll('button');
show.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>commit</button>
<button>discard</button>
`
);
commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>commit</button>
<button>discard</button>
failed
`
);
}
});

@ -0,0 +1,18 @@
<script>
import { fork } from 'svelte';
let show = $state(false);
let f;
</script>
<button onclick={() => f = fork(() => show = true)}>show</button>
<button onclick={() => f.commit()}>commit</button>
<button onclick={() => f.discard()}>discard</button>
<svelte:boundary>
{#if show}
{await Promise.reject('boom')}
{/if}
{#snippet failed()}
failed
{/snippet}
</svelte:boundary>

@ -0,0 +1,30 @@
import { flushSync } from 'svelte';
import { HMR } from 'svelte/internal/client';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true,
hmr: true
},
async test({ assert, target }) {
const [btn] = target.querySelectorAll('button');
btn.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>show</button> component`);
// Simulate HMR swap on the child component.
const hidden = './_output/client/Component' + '.svelte.js';
const mod = await import(/* vite-ignore */ hidden);
const hmr_data = mod.default[HMR];
const fake_incoming = {
// Fake a new component, else HMR source's equality check will ignore the update
[HMR]: { fn: /** @param {any} args */ (...args) => hmr_data.fn(...args), current: null }
};
hmr_data.update(fake_incoming);
flushSync();
assert.htmlEqual(target.innerHTML, `<button>show</button> component`);
}
});

@ -0,0 +1,9 @@
<script>
import Component from "./Component.svelte";
let C = $state(null);
</script>
<button onclick={() => C = Component}>show</button>
<C />

@ -21,7 +21,7 @@
"polka": "^1.0.0-next.25",
"svelte": "workspace:*",
"tinyglobby": "^0.2.12",
"vite": "^7.1.11",
"vite": "^7.3.2",
"vite-plugin-devtools-json": "^1.0.0",
"vite-plugin-inspect": "^11.3.3"
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save