From 3feb34a9922bf64610d009d44b0865a7ae88c2ef Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Thu, 20 Aug 2026 09:26:06 -0400 Subject: [PATCH] chore: deduplicate client/server context helpers (#18580) The server copy of `createContext` shipped without the `missing_context` throw and #17580 had to hand-mirror the client body back in, so this duplication has already cost a bug. This moves the realm-independent parts, the `createContext` tuple, `get_parent_context`, and `get_or_init_context_map`, into `internal/shared/context.js`, and each realm keeps its public context functions as thin wrappers over its own state. --- .changeset/shared-context-helpers.md | 5 ++ .../svelte/src/internal/client/context.js | 52 +++--------------- .../svelte/src/internal/server/context.js | 55 +++---------------- .../svelte/src/internal/shared/context.js | 52 ++++++++++++++++++ 4 files changed, 73 insertions(+), 91 deletions(-) create mode 100644 .changeset/shared-context-helpers.md create mode 100644 packages/svelte/src/internal/shared/context.js diff --git a/.changeset/shared-context-helpers.md b/.changeset/shared-context-helpers.md new file mode 100644 index 0000000000..ecdfcb9a0e --- /dev/null +++ b/.changeset/shared-context-helpers.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: deduplicate client and server context helpers diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 0baef5c63e..f4890d0409 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -6,6 +6,7 @@ import { create_user_effect } from './reactivity/effects.js'; import { async_mode_flag, legacy_mode_flag } from '../flags/index.js'; import { FILENAME } from '../../constants.js'; import { BRANCH_EFFECT } from './constants.js'; +import { create_context, get_or_init_context_map } from '../shared/context.js'; /** @type {ComponentContext | null} */ export let component_context = null; @@ -79,18 +80,9 @@ export function set_dev_current_component_function(fn) { * @since 5.40.0 */ export function createContext() { - const key = {}; - - return [ - () => { - if (!hasContext(key)) { - e.missing_context(); - } - - return getContext(key); - }, - (context) => setContext(key, context) - ]; + return /** @type {[() => T, (context: T) => T]} */ ( + create_context(getContext, setContext, hasContext) + ); } /** @@ -104,7 +96,7 @@ export function createContext() { * @returns {T} */ export function getContext(key) { - const context_map = get_or_init_context_map('getContext'); + const context_map = get_or_init_context_map(component_context, 'getContext'); const result = /** @type {T} */ (context_map.get(key)); return result; } @@ -124,7 +116,7 @@ export function getContext(key) { * @returns {T} */ export function setContext(key, context) { - const context_map = get_or_init_context_map('setContext'); + const context_map = get_or_init_context_map(component_context, 'setContext'); if (async_mode_flag) { var flags = /** @type {Effect} */ (active_effect).f; @@ -151,7 +143,7 @@ export function setContext(key, context) { * @returns {boolean} */ export function hasContext(key) { - const context_map = get_or_init_context_map('hasContext'); + const context_map = get_or_init_context_map(component_context, 'hasContext'); return context_map.has(key); } @@ -164,7 +156,7 @@ export function hasContext(key) { * @returns {T} */ export function getAllContexts() { - const context_map = get_or_init_context_map('getAllContexts'); + const context_map = get_or_init_context_map(component_context, 'getAllContexts'); return /** @type {T} */ (context_map); } @@ -229,31 +221,3 @@ export function pop(component) { export function is_runes() { return !legacy_mode_flag || (component_context !== null && component_context.l === null); } - -/** - * @param {string} name - * @returns {Map} - */ -function get_or_init_context_map(name) { - if (component_context === null) { - e.lifecycle_outside_component(name); - } - - return (component_context.c ??= new Map(get_parent_context(component_context) || undefined)); -} - -/** - * @param {ComponentContext} component_context - * @returns {Map | null} - */ -function get_parent_context(component_context) { - let parent = component_context.p; - while (parent !== null) { - const context_map = parent.c; - if (context_map !== null) { - return context_map; - } - parent = parent.p; - } - return null; -} diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index 6a7dc1f883..de11c11282 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -1,6 +1,6 @@ /** @import { SSRContext } from '#server' */ import { DEV } from 'esm-env'; -import * as e from './errors.js'; +import { create_context, get_or_init_context_map } from '../shared/context.js'; /** @type {SSRContext | null} */ export var ssr_context = null; @@ -16,18 +16,9 @@ export function set_ssr_context(v) { * @since 5.40.0 */ export function createContext() { - const key = {}; - - return [ - () => { - if (!hasContext(key)) { - e.missing_context(); - } - - return getContext(key); - }, - (context) => setContext(key, context) - ]; + return /** @type {[() => T, (context: T) => T]} */ ( + create_context(getContext, setContext, hasContext) + ); } /** @@ -36,7 +27,7 @@ export function createContext() { * @returns {T} */ export function getContext(key) { - const context_map = get_or_init_context_map('getContext'); + const context_map = get_or_init_context_map(ssr_context, 'getContext'); const result = /** @type {T} */ (context_map.get(key)); return result; @@ -49,7 +40,7 @@ export function getContext(key) { * @returns {T} */ export function setContext(key, context) { - get_or_init_context_map('setContext').set(key, context); + get_or_init_context_map(ssr_context, 'setContext').set(key, context); return context; } @@ -58,24 +49,12 @@ export function setContext(key, context) { * @returns {boolean} */ export function hasContext(key) { - return get_or_init_context_map('hasContext').has(key); + return get_or_init_context_map(ssr_context, 'hasContext').has(key); } /** @returns {Map} */ export function getAllContexts() { - return get_or_init_context_map('getAllContexts'); -} - -/** - * @param {string} name - * @returns {Map} - */ -function get_or_init_context_map(name) { - if (ssr_context === null) { - e.lifecycle_outside_component(name); - } - - return (ssr_context.c ??= new Map(get_parent_context(ssr_context) || undefined)); + return get_or_init_context_map(ssr_context, 'getAllContexts'); } /** @@ -94,24 +73,6 @@ export function pop() { ssr_context = /** @type {SSRContext} */ (ssr_context).p; } -/** - * @param {SSRContext} ssr_context - * @returns {Map | null} - */ -function get_parent_context(ssr_context) { - let parent = ssr_context.p; - - while (parent !== null) { - const context_map = parent.c; - if (context_map !== null) { - return context_map; - } - parent = parent.p; - } - - return null; -} - /** * Wraps an `await` expression in such a way that the component context that was * active before the expression evaluated can be reapplied afterwards — diff --git a/packages/svelte/src/internal/shared/context.js b/packages/svelte/src/internal/shared/context.js new file mode 100644 index 0000000000..943e15e415 --- /dev/null +++ b/packages/svelte/src/internal/shared/context.js @@ -0,0 +1,52 @@ +import { lifecycle_outside_component, missing_context } from './errors.js'; + +/** + * @template T + * @param {(key: object) => T} get_context + * @param {(key: object, context: T) => T} set_context + * @param {(key: object) => boolean} has_context + * @returns {[() => T, (context: T) => T]} + */ +export function create_context(get_context, set_context, has_context) { + const key = {}; + + return [ + () => { + if (!has_context(key)) { + missing_context(); + } + + return get_context(key); + }, + (context) => set_context(key, context) + ]; +} + +/** + * @typedef {{ p: Context | null, c: Map | null }} Context + */ + +/** + * @param {Context} context + * @returns {Map | null} + */ +function get_parent_context(context) { + let parent = context.p; + while (parent !== null && parent.c === null) { + parent = parent.p; + } + return parent?.c ?? null; +} + +/** + * @param {Context | null} context + * @param {string} name + * @returns {Map} + */ +export function get_or_init_context_map(context, name) { + if (context === null) { + lifecycle_outside_component(name); + } + + return (context.c ??= new Map(get_parent_context(context) || undefined)); +}