From f3c55e8e6c26bc8c006c84a656c845584abf3eb4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 15:48:29 -0400 Subject: [PATCH] feat: add `createContext` utility for type-safe context (#16948) * feat: add `createContext` utility for type-safe context * regenerate --- .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 | 4 ++++ 12 files changed, 110 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 0000000000..1107d7ef20 --- /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 6c31aaafd0..07e13dea45 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 4b4d332202..e3959034a3 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 85eeab7de9..337cbb500b 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 f193c46894..223ce6a4cd 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 cad75546d4..ea63072a37 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 c59b2d260a..1813bfbf78 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 6bcc35016a..669cdd96a7 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} 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 0000000000..3e39d5043e --- /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 0000000000..4ae28e68bd --- /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 0000000000..8d3c50ba55 --- /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 6dc6629faa..58e3285e4a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -448,6 +448,10 @@ 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`. * Must be called during component initialisation.