diff --git a/.changeset/slick-teeth-exist.md b/.changeset/slick-teeth-exist.md new file mode 100644 index 0000000000..aeeb0f41b9 --- /dev/null +++ b/.changeset/slick-teeth-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better error message for global variable assignments diff --git a/.changeset/small-geckos-camp.md b/.changeset/small-geckos-camp.md deleted file mode 100644 index 622cbbbfa0..0000000000 --- a/.changeset/small-geckos-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -feat: experimental `fork` API diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 74a0674dba..3f1cb8f76b 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -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 diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index d63548d3e1..51e6317491 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -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 diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index b5fe51539d..ae7d811b2e 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1d50920d4f..1f8746e72d 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.41.4", + "version": "5.42.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index d7b682da08..cc4376a0c2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -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()'); } diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 24dc9e4fb8..c2f7861b78 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -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. diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 751a35321a..ffdb342adb 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -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) { diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index 09150d6ee4..34ba508984 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -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); diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 98be92d4b2..4688637f5d 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -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); diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 6c83a453d5..dcbbf14e20 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -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); } diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 2a433ed8f9..8a5fde4f3b 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -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'; diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 9baacacd0d..49cef451b3 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -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; /** diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index fdeb111a4d..ab83050cd0 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -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} */ - 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(); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b6a50acc4d..1eb640ad26 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -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; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 5b13426233..2cf7c80b25 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -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(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2e6f05b4b1..49396d6feb 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -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); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index d24218c4d3..deb3e82986 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -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; @@ -16,6 +16,8 @@ export type ComponentContext = { c: null | Map; /** 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 diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index f5f47c6056..8d50b983ce 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.41.4'; +export const VERSION = '5.42.1'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index bf708878a3..d0ec8b6e44 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -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; } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js new file mode 100644 index 0000000000..be73968a88 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js @@ -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' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte new file mode 100644 index 0000000000..8e770c214b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js index bde65a499f..2bcb129b12 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js @@ -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); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index 16318a3b44..747648e83f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -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); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte new file mode 100644 index 0000000000..2c7fd5d43d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte @@ -0,0 +1,7 @@ + + +

{greeting}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte new file mode 100644 index 0000000000..9a493c5b75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte @@ -0,0 +1,9 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js new file mode 100644 index 0000000000..041f67a39e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'async-server'], + ssrHtml: `

hi

`, + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

hi

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte new file mode 100644 index 0000000000..01b46bda93 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js index 400495050c..57f60c2b44 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js @@ -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')); } } diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js new file mode 100644 index 0000000000..37f4b2814c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + error: 'x is not defined', + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js new file mode 100644 index 0000000000..198b8f89e7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js @@ -0,0 +1 @@ +x = 1; diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte new file mode 100644 index 0000000000..0ac6956b1d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte @@ -0,0 +1,3 @@ +