Merge branch 'main' into out-of-order-rendering

pull/17038/head
Rich Harris 1 week ago
commit e79ea01596

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: better error message for global variable assignments

@ -1,5 +0,0 @@
---
"svelte": patch
---
feat: experimental `fork` API

@ -149,7 +149,7 @@ This restriction only applies when using the `experimental.async` option, which
### fork_discarded ### fork_discarded
``` ```
Cannot commit a fork that was already committed or discarded Cannot commit a fork that was already discarded
``` ```
### fork_timing ### fork_timing

@ -1,5 +1,23 @@
# svelte # svelte
## 5.42.1
### Patch Changes
- fix: ignore fork `discard()` after `commit()` ([#17034](https://github.com/sveltejs/svelte/pull/17034))
## 5.42.0
### Minor Changes
- feat: experimental `fork` API ([#17004](https://github.com/sveltejs/svelte/pull/17004))
### Patch Changes
- fix: always allow `setContext` before first await in component ([#17031](https://github.com/sveltejs/svelte/pull/17031))
- fix: less confusing names for inspect errors ([#17026](https://github.com/sveltejs/svelte/pull/17026))
## 5.41.4 ## 5.41.4
### Patch Changes ### Patch Changes

@ -114,7 +114,7 @@ This restriction only applies when using the `experimental.async` option, which
## fork_discarded ## fork_discarded
> Cannot commit a fork that was already committed or discarded > Cannot commit a fork that was already discarded
## fork_timing ## fork_timing

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

@ -22,7 +22,10 @@ export function validate_assignment(node, argument, context) {
const binding = context.state.scope.get(argument.name); const binding = context.state.scope.get(argument.name);
if (context.state.analysis.runes) { if (context.state.analysis.runes) {
if (binding?.node === context.state.analysis.props_id) { if (
context.state.analysis.props_id != null &&
binding?.node === context.state.analysis.props_id
) {
e.constant_assignment(node, '$props.id()'); e.constant_assignment(node, '$props.id()');
} }

@ -13,6 +13,7 @@ export const INERT = 1 << 13;
export const DESTROYED = 1 << 14; export const DESTROYED = 1 << 14;
// Flags exclusive to effects // Flags exclusive to effects
/** Set once an effect that should run synchronously has run */
export const EFFECT_RAN = 1 << 15; export const EFFECT_RAN = 1 << 15;
/** /**
* 'Transparent' effects do not create a transition boundary. * 'Transparent' effects do not create a transition boundary.

@ -128,7 +128,11 @@ export function setContext(key, context) {
if (async_mode_flag) { if (async_mode_flag) {
var flags = /** @type {Effect} */ (active_effect).f; var flags = /** @type {Effect} */ (active_effect).f;
var valid = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0; var valid =
!active_reaction &&
(flags & BRANCH_EFFECT) !== 0 &&
// pop() runs synchronously, so this indicates we're setting context after an await
!(/** @type {ComponentContext} */ (component_context).i);
if (!valid) { if (!valid) {
e.set_context_after_init(); e.set_context_after_init();
@ -173,6 +177,7 @@ export function getAllContexts() {
export function push(props, runes = false, fn) { export function push(props, runes = false, fn) {
component_context = { component_context = {
p: component_context, p: component_context,
i: false,
c: null, c: null,
e: null, e: null,
s: props, s: props,
@ -208,6 +213,8 @@ export function pop(component) {
context.x = component; context.x = component;
} }
context.i = true;
component_context = context.p; component_context = context.p;
if (DEV) { if (DEV) {

@ -33,8 +33,17 @@ export function inspect(get_value, inspector, show_stack = false) {
inspector(...snap); inspector(...snap);
if (!initial) { if (!initial) {
const stack = get_stack('$inspect(...)');
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(get_stack('UpdatedAt'));
if (stack) {
// eslint-disable-next-line no-console
console.groupCollapsed('stack trace');
// eslint-disable-next-line no-console
console.log(stack);
// eslint-disable-next-line no-console
console.groupEnd();
}
} }
} else { } else {
inspector(initial ? 'init' : 'update', ...snap); inspector(initial ? 'init' : 'update', ...snap);

@ -179,8 +179,7 @@ export function get_stack(label) {
}); });
define_property(error, 'name', { define_property(error, 'name', {
// 'Error' suffix is required for stack traces to be rendered properly value: label
value: `${label}Error`
}); });
return /** @type {Error & { stack: string }} */ (error); return /** @type {Error & { stack: string }} */ (error);

@ -29,7 +29,7 @@ export function handle_error(error) {
// if the error occurred while creating this subtree, we let it // if the error occurred while creating this subtree, we let it
// bubble up until it hits a boundary that can handle it // bubble up until it hits a boundary that can handle it
if ((effect.f & BOUNDARY_EFFECT) === 0) { if ((effect.f & BOUNDARY_EFFECT) === 0) {
if (!effect.parent && error instanceof Error) { if (DEV && !effect.parent && error instanceof Error) {
apply_adjustments(error); apply_adjustments(error);
} }
@ -61,7 +61,7 @@ export function invoke_error_boundary(error, effect) {
effect = effect.parent; effect = effect.parent;
} }
if (error instanceof Error) { if (DEV && error instanceof Error) {
apply_adjustments(error); apply_adjustments(error);
} }

@ -262,12 +262,12 @@ export function flush_sync_in_effect() {
} }
/** /**
* Cannot commit a fork that was already committed or discarded * Cannot commit a fork that was already discarded
* @returns {never} * @returns {never}
*/ */
export function fork_discarded() { export function fork_discarded() {
if (DEV) { if (DEV) {
const error = new Error(`fork_discarded\nCannot commit a fork that was already committed or discarded\nhttps://svelte.dev/e/fork_discarded`); const error = new Error(`fork_discarded\nCannot commit a fork that was already discarded\nhttps://svelte.dev/e/fork_discarded`);
error.name = 'Svelte error'; error.name = 'Svelte error';

@ -53,7 +53,7 @@ export function proxy(value) {
var is_proxied_array = is_array(value); var is_proxied_array = is_array(value);
var version = source(0); var version = source(0);
var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; var stack = DEV && tracing_mode_flag ? get_stack('created at') : null;
var parent_version = update_version; var parent_version = update_version;
/** /**

@ -913,28 +913,36 @@ export function fork(fn) {
e.fork_timing(); e.fork_timing();
} }
const batch = Batch.ensure(); var batch = Batch.ensure();
batch.is_fork = true; batch.is_fork = true;
const settled = batch.settled(); var committed = false;
var settled = batch.settled();
flushSync(fn); flushSync(fn);
// revert state changes // revert state changes
for (const [source, value] of batch.previous) { for (var [source, value] of batch.previous) {
source.v = value; source.v = value;
} }
return { return {
commit: async () => { commit: async () => {
if (committed) {
await settled;
return;
}
if (!batches.has(batch)) { if (!batches.has(batch)) {
e.fork_discarded(); e.fork_discarded();
} }
committed = true;
batch.is_fork = false; batch.is_fork = false;
// apply changes // apply changes
for (const [source, value] of batch.current) { for (var [source, value] of batch.current) {
source.v = value; source.v = value;
} }
@ -945,9 +953,9 @@ export function fork(fn) {
// TODO maybe there's a better implementation? // TODO maybe there's a better implementation?
flushSync(() => { flushSync(() => {
/** @type {Set<Effect>} */ /** @type {Set<Effect>} */
const eager_effects = new Set(); var eager_effects = new Set();
for (const source of batch.current.keys()) { for (var source of batch.current.keys()) {
mark_eager_effects(source, eager_effects); mark_eager_effects(source, eager_effects);
} }
@ -959,7 +967,7 @@ export function fork(fn) {
await settled; await settled;
}, },
discard: () => { discard: () => {
if (batches.has(batch)) { if (!committed && batches.has(batch)) {
batches.delete(batch); batches.delete(batch);
batch.discard(); batch.discard();
} }

@ -86,7 +86,7 @@ export function derived(fn) {
}; };
if (DEV && tracing_mode_flag) { if (DEV && tracing_mode_flag) {
signal.created = get_stack('CreatedAt'); signal.created = get_stack('created at');
} }
return signal; return signal;

@ -76,7 +76,7 @@ export function source(v, stack) {
}; };
if (DEV && tracing_mode_flag) { if (DEV && tracing_mode_flag) {
signal.created = stack ?? get_stack('CreatedAt'); signal.created = stack ?? get_stack('created at');
signal.updated = null; signal.updated = null;
signal.set_during_effect = false; signal.set_during_effect = false;
signal.trace = null; signal.trace = null;
@ -186,7 +186,7 @@ export function internal_set(source, value) {
if (DEV) { if (DEV) {
if (tracing_mode_flag || active_effect !== null) { if (tracing_mode_flag || active_effect !== null) {
const error = get_stack('UpdatedAt'); const error = get_stack('updated at');
if (error !== null) { if (error !== null) {
source.updated ??= new Map(); source.updated ??= new Map();

@ -609,7 +609,7 @@ export function get(signal) {
if (!tracking && !untracking && !was_read) { if (!tracking && !untracking && !was_read) {
w.await_reactivity_loss(/** @type {string} */ (signal.label)); w.await_reactivity_loss(/** @type {string} */ (signal.label));
var trace = get_stack('TracedAt'); var trace = get_stack('traced at');
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
if (trace) console.warn(trace); if (trace) console.warn(trace);
} }
@ -628,7 +628,7 @@ export function get(signal) {
if (signal.trace) { if (signal.trace) {
signal.trace(); signal.trace();
} else { } else {
trace = get_stack('TracedAt'); trace = get_stack('traced at');
if (trace) { if (trace) {
var entry = tracing_expressions.entries.get(signal); var entry = tracing_expressions.entries.get(signal);

@ -1,6 +1,6 @@
import type { Store } from '#shared'; import type { Store } from '#shared';
import { STATE_SYMBOL } from './constants.js'; import { STATE_SYMBOL } from './constants.js';
import type { Effect, Source, Value, Reaction } from './reactivity/types.js'; import type { Effect, Source, Value } from './reactivity/types.js';
type EventCallback = (event: Event) => boolean; type EventCallback = (event: Event) => boolean;
export type EventCallbackMap = Record<string, EventCallback | EventCallback[]>; export type EventCallbackMap = Record<string, EventCallback | EventCallback[]>;
@ -16,6 +16,8 @@ export type ComponentContext = {
c: null | Map<unknown, unknown>; c: null | Map<unknown, unknown>;
/** deferred effects */ /** deferred effects */
e: null | Array<() => void | (() => void)>; e: null | Array<() => void | (() => void)>;
/** True if initialized, i.e. pop() ran */
i: boolean;
/** /**
* props needed for legacy mode lifecycle functions, and for `createEventDispatcher` * props needed for legacy mode lifecycle functions, and for `createEventDispatcher`
* @deprecated remove in 6.0 * @deprecated remove in 6.0

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

@ -201,7 +201,15 @@ export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true';
* @param {any[]} logs * @param {any[]} logs
*/ */
export function normalise_inspect_logs(logs) { export function normalise_inspect_logs(logs) {
return logs.map((log) => { /** @type {string[]} */
const normalised = [];
for (const log of logs) {
if (log === 'stack trace') {
// ignore `console.group('stack trace')` in default `$inspect(...)` output
continue;
}
if (log instanceof Error) { if (log instanceof Error) {
const last_line = log.stack const last_line = log.stack
?.trim() ?.trim()
@ -210,11 +218,13 @@ export function normalise_inspect_logs(logs) {
const match = last_line && /(at .+) /.exec(last_line); const match = last_line && /(at .+) /.exec(last_line);
return match && match[1]; if (match) normalised.push(match[1]);
} else {
normalised.push(log);
} }
}
return log; return normalised;
});
} }
/** /**

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test() {
// else runtime_error is checked too soon
await tick();
},
runtime_error: 'set_context_after_init'
});

@ -0,0 +1,7 @@
<script>
import { setContext } from 'svelte';
await Promise.resolve('hi');
setContext('key', 'value');
</script>

@ -17,7 +17,7 @@ export default test({
'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`'
); );
assert.equal(warnings[1].name, 'TracedAtError'); assert.equal(warnings[1].name, 'traced at');
assert.equal(warnings.length, 2); assert.equal(warnings.length, 2);
} }

@ -20,7 +20,7 @@ export default test({
'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`' 'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`'
); );
assert.equal(warnings[1].name, 'TracedAtError'); assert.equal(warnings[1].name, 'traced at');
assert.equal(warnings.length, 2); assert.equal(warnings.length, 2);
} }

@ -0,0 +1,7 @@
<script lang="ts">
import { getContext } from "svelte";
let greeting = getContext("greeting");
</script>
<p>{greeting}</p>

@ -0,0 +1,9 @@
<script lang="ts">
import { setContext } from "svelte";
import Inner from "./Inner.svelte";
setContext("greeting", "hi");
await Promise.resolve();
</script>
<Inner />

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'async-server'],
ssrHtml: `<p>hi</p>`,
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>hi</p>');
}
});

@ -0,0 +1,7 @@
<script lang="ts">
import Outer from "./Outer.svelte";
await Promise.resolve();
</script>
<Outer />

@ -14,7 +14,7 @@ export default test({
try { try {
flushSync(() => button.click()); flushSync(() => button.click());
} catch (e) { } catch (e) {
assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be UpdatedAtError assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be 'updated at'
assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded')); assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded'));
} }
} }

@ -0,0 +1,6 @@
import { test } from '../../test';
export default test({
error: 'x is not defined',
async test() {}
});
Loading…
Cancel
Save