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
```
Cannot commit a fork that was already committed or discarded
Cannot commit a fork that was already discarded
```
### fork_timing

@ -1,5 +1,23 @@
# 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
### Patch Changes

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

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

@ -22,7 +22,10 @@ export function validate_assignment(node, argument, context) {
const binding = context.state.scope.get(argument.name);
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()');
}

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

@ -128,7 +128,11 @@ export function setContext(key, context) {
if (async_mode_flag) {
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) {
e.set_context_after_init();
@ -173,6 +177,7 @@ export function getAllContexts() {
export function push(props, runes = false, fn) {
component_context = {
p: component_context,
i: false,
c: null,
e: null,
s: props,
@ -208,6 +213,8 @@ export function pop(component) {
context.x = component;
}
context.i = true;
component_context = context.p;
if (DEV) {

@ -33,8 +33,17 @@ export function inspect(get_value, inspector, show_stack = false) {
inspector(...snap);
if (!initial) {
const stack = get_stack('$inspect(...)');
// 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 {
inspector(initial ? 'init' : 'update', ...snap);

@ -179,8 +179,7 @@ export function get_stack(label) {
});
define_property(error, 'name', {
// 'Error' suffix is required for stack traces to be rendered properly
value: `${label}Error`
value: label
});
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
// bubble up until it hits a boundary that can handle it
if ((effect.f & BOUNDARY_EFFECT) === 0) {
if (!effect.parent && error instanceof Error) {
if (DEV && !effect.parent && error instanceof Error) {
apply_adjustments(error);
}
@ -61,7 +61,7 @@ export function invoke_error_boundary(error, effect) {
effect = effect.parent;
}
if (error instanceof Error) {
if (DEV && error instanceof 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}
*/
export function fork_discarded() {
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';

@ -53,7 +53,7 @@ export function proxy(value) {
var is_proxied_array = is_array(value);
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;
/**

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

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

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

@ -609,7 +609,7 @@ export function get(signal) {
if (!tracking && !untracking && !was_read) {
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
if (trace) console.warn(trace);
}
@ -628,7 +628,7 @@ export function get(signal) {
if (signal.trace) {
signal.trace();
} else {
trace = get_stack('TracedAt');
trace = get_stack('traced at');
if (trace) {
var entry = tracing_expressions.entries.get(signal);

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

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

@ -201,7 +201,15 @@ export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true';
* @param {any[]} 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) {
const last_line = log.stack
?.trim()
@ -210,11 +218,13 @@ export function normalise_inspect_logs(logs) {
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`'
);
assert.equal(warnings[1].name, 'TracedAtError');
assert.equal(warnings[1].name, 'traced at');
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`'
);
assert.equal(warnings[1].name, 'TracedAtError');
assert.equal(warnings[1].name, 'traced at');
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 {
flushSync(() => button.click());
} 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'));
}
}

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