From 74c626aa55e2ca158b2f4a039c2c751948f433da Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 15:22:43 -0400 Subject: [PATCH 1/2] feat: add `createContext` utility for type-safe context --- .changeset/neat-melons-cheer.md | 5 +++++ .../98-reference/.generated/shared-errors.md | 8 ++++++++ .../svelte/messages/shared-errors/errors.md | 6 ++++++ packages/svelte/src/index-client.js | 8 +++++++- packages/svelte/src/index-server.js | 8 +++++++- .../svelte/src/internal/client/context.js | 20 +++++++++++++++++++ .../svelte/src/internal/server/context.js | 9 +++++++++ packages/svelte/src/internal/shared/errors.js | 16 +++++++++++++++ .../samples/create-context/Child.svelte | 7 +++++++ .../samples/create-context/_config.js | 5 +++++ .../samples/create-context/main.svelte | 16 +++++++++++++++ packages/svelte/types/index.d.ts | 1 + 12 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 .changeset/neat-melons-cheer.md create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context/main.svelte diff --git a/.changeset/neat-melons-cheer.md b/.changeset/neat-melons-cheer.md new file mode 100644 index 000000000000..1107d7ef20a0 --- /dev/null +++ b/.changeset/neat-melons-cheer.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `createContext` utility for type-safe context diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 6c31aaafd0df..07e13dea459b 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -60,6 +60,14 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +### missing_context + +``` +Context was not set in a parent component +``` + +The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. + ### snippet_without_render_tag ``` diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 4b4d3322028d..e3959034a3c3 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -52,6 +52,12 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +## missing_context + +> Context was not set in a parent component + +The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. + ## snippet_without_render_tag > Attempted 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()}`. diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 85eeab7de989..337cbb500b39 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -242,7 +242,13 @@ function init_update_callbacks(context) { } export { flushSync } from './internal/client/reactivity/batch.js'; -export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js'; +export { + createContext, + getContext, + getAllContexts, + hasContext, + setContext +} from './internal/client/context.js'; export { hydrate, mount, unmount } from './internal/client/render.js'; export { tick, untrack, settled } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index f193c4689474..223ce6a4cde1 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -39,6 +39,12 @@ export async function settled() {} export { getAbortSignal } from './internal/server/abort-signal.js'; -export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; +export { + createContext, + getAllContexts, + getContext, + hasContext, + setContext +} from './internal/server/context.js'; export { createRawSnippet } from './internal/server/blocks/snippet.js'; diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index cad75546d4b4..ea63072a377f 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -69,6 +69,26 @@ export function set_dev_current_component_function(fn) { dev_current_component_function = fn; } +/** + * Returns a `[get, set]` pair of functions for working with context in a type-safe way. + * @template T + * @returns {[() => T, (context: T) => T]} + */ +export function createContext() { + const key = {}; + + return [ + () => { + if (!hasContext(key)) { + e.missing_context(); + } + + return getContext(key); + }, + (context) => setContext(key, context) + ]; +} + /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index c59b2d260afb..1813bfbf7848 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -10,6 +10,15 @@ export function set_ssr_context(v) { ssr_context = v; } +/** + * @template T + * @returns {[() => T, (context: T) => T]} + */ +export function createContext() { + const key = {}; + return [() => getContext(key), (context) => setContext(key, context)]; +} + /** * @template T * @param {any} key diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 6bcc35016a70..8db7cc09ec4d 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -98,4 +98,20 @@ export function svelte_element_invalid_this_value() { } else { throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`); } +} + +/** + * Context was not set in a parent component + * @returns {never} + */ +export function missing_context() { + if (DEV) { + const error = new Error(`missing_context\nContext was not set in a parent component\nhttps://svelte.dev/e/missing_context`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/missing_context`); + } } \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte b/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte new file mode 100644 index 000000000000..3e39d5043eff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte @@ -0,0 +1,7 @@ + + +

{message}

diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/_config.js b/packages/svelte/tests/runtime-runes/samples/create-context/_config.js new file mode 100644 index 000000000000..4ae28e68bd05 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

hello

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte b/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte new file mode 100644 index 000000000000..8d3c50ba5539 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 6dc6629faad5..e87286362e5e 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -448,6 +448,7 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + export function createContext(): [() => T, (context: T) => T]; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. From c47eaf34749b93d07607ce8ae49071f3dfb393c7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 15:35:20 -0400 Subject: [PATCH 2/2] regenerate --- packages/svelte/src/internal/shared/errors.js | 32 +++++++++---------- packages/svelte/types/index.d.ts | 3 ++ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 8db7cc09ec4d..669cdd96a7f3 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -51,6 +51,22 @@ export function lifecycle_outside_component(name) { } } +/** + * Context was not set in a parent component + * @returns {never} + */ +export function missing_context() { + if (DEV) { + const error = new Error(`missing_context\nContext was not set in a parent component\nhttps://svelte.dev/e/missing_context`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/missing_context`); + } +} + /** * Attempted 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()}`. * @returns {never} @@ -98,20 +114,4 @@ export function svelte_element_invalid_this_value() { } else { throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`); } -} - -/** - * Context was not set in a parent component - * @returns {never} - */ -export function missing_context() { - if (DEV) { - const error = new Error(`missing_context\nContext was not set in a parent component\nhttps://svelte.dev/e/missing_context`); - - error.name = 'Svelte error'; - - throw error; - } else { - throw new Error(`https://svelte.dev/e/missing_context`); - } } \ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index e87286362e5e..58e3285e4ad1 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -448,6 +448,9 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Returns a `[get, set]` pair of functions for working with context in a type-safe way. + * */ export function createContext(): [() => T, (context: T) => T]; /** * Retrieves the context that belongs to the closest parent component with the specified `key`.