From bca95a87f193ed35ae74f1b7cb884ecb83891550 Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Thu, 21 May 2026 10:14:46 -0400 Subject: [PATCH 1/2] Initial migration to Solid 2 beta 13 --- .changeset/context-solid2-migration.md | 14 +++++++++ packages/context/CHANGELOG.md | 14 +++++++++ packages/context/README.md | 14 +++++---- packages/context/dev/index.tsx | 7 ++--- packages/context/package.json | 8 +++-- packages/context/src/index.ts | 33 ++++++++++----------- packages/context/test/index.test.tsx | 41 +++++++++++++++----------- packages/context/test/server.test.tsx | 10 +++---- pnpm-lock.yaml | 7 +++-- 9 files changed, 92 insertions(+), 56 deletions(-) create mode 100644 .changeset/context-solid2-migration.md diff --git a/.changeset/context-solid2-migration.md b/.changeset/context-solid2-migration.md new file mode 100644 index 000000000..ef4ba0533 --- /dev/null +++ b/.changeset/context-solid2-migration.md @@ -0,0 +1,14 @@ +--- +"@solid-primitives/context": major +--- + +Migrate to Solid.js v2.0 (beta.13) + +## Breaking Changes + +**Peer dependencies**: `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13` are now required. + +- `Context` is now used directly as the provider component — `Context.Provider` no longer exists in Solid 2.0. `createContextProvider` and `MultiProvider` both reflect this change. +- `ContextProviderComponent` is now a proper export from `solid-js`; the previous workaround importing from an internal `node_modules` path has been removed. +- `JSX.Element` replaced with `Element` from `solid-js` throughout the public API types, matching Solid 2.0's renderer-neutral type model. +- `MultiProvider` no longer falls back to accessing `.Provider` on non-function items — contexts passed in `values` must be functions (which all `Context` objects now are in Solid 2.0). diff --git a/packages/context/CHANGELOG.md b/packages/context/CHANGELOG.md index 1d7a5e940..681999f78 100644 --- a/packages/context/CHANGELOG.md +++ b/packages/context/CHANGELOG.md @@ -1,5 +1,19 @@ # @solid-primitives/context +## 1.0.0 + +### Major Changes + +- Migrate to Solid.js v2.0 (beta.13) + +#### Breaking Changes + +- **Peer dependencies**: `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13` are now required. +- `Context` is now used directly as the provider component — `Context.Provider` no longer exists in Solid 2.0. `createContextProvider` and `MultiProvider` both reflect this change. +- `ContextProviderComponent` is now a proper export from `solid-js`; the previous workaround importing from an internal `node_modules` path has been removed. +- `JSX.Element` replaced with `Element` from `solid-js` throughout the public API types. +- `MultiProvider` no longer falls back to accessing `.Provider` on non-function items — contexts passed in `values` must be functions (which all `Context` objects are in Solid 2.0). + ## 0.3.2 ### Patch Changes diff --git a/packages/context/README.md b/packages/context/README.md index 4b2b0eae7..7b21982ab 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -23,6 +23,8 @@ pnpm add @solid-primitives/context yarn add @solid-primitives/context ``` +Requires `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13`. + ## `createContextProvider` Create the Context Provider component and useContext function with types inferred from the factory function. @@ -99,17 +101,17 @@ It will work exactly like nesting multiple providers as separate components, but import { MultiProvider } from "@solid-primitives/context"; // before - - - + + + - - -; + + +; // after { const TestCtx = createContext<{ title: string }>(); const BoundProvider: FlowComponent = props => ( - {props.children} + {props.children} ); const App: Component = () => { @@ -47,11 +47,8 @@ const App: Component = () => { ", "license": "MIT", @@ -51,10 +51,12 @@ "primitives" ], "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.13", + "solid-js": "^2.0.0-beta.13" }, "typesVersions": {}, "devDependencies": { - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.13", + "solid-js": "2.0.0-beta.13" } } diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index a38597c31..6a233daee 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -2,18 +2,18 @@ import { createContext, createComponent, useContext, - type JSX, + type Element, type Context, + type ContextProviderComponent, type FlowComponent, } from "solid-js"; -import type { ContextProviderComponent } from "../node_modules/solid-js/types/reactive/signal.js"; export type ContextProviderProps = { - children?: JSX.Element; + children?: Element; } & Record; export type ContextProvider = ( - props: { children: JSX.Element } & T, -) => JSX.Element; + props: { children: Element } & T, +) => Element; /** * Create the Context Provider component and useContext function with types inferred from the factory function. @@ -47,10 +47,10 @@ export function createContextProvider( factoryFn: (props: P) => T, defaults?: T, ): [provider: ContextProvider

, useContext: () => T | undefined] { - const ctx = createContext(defaults); + const ctx = createContext(defaults); return [ props => { - return createComponent(ctx.Provider, { + return createComponent(ctx, { value: factoryFn(props), get children() { return props.children; @@ -79,16 +79,16 @@ Type validation of the `values` array thanks to the amazing @otonashixav (https: * @example * ```tsx * // before - * - * + * + * * - * - * + * + * * * // after * * * @@ -103,15 +103,15 @@ export function MultiProvider(props ] | FlowComponent; }; - children: JSX.Element; -}): JSX.Element { + children: Element; +}): Element { const { values } = props; const fn = (i: number) => { let item: any = values[i]; if (!item) return props.children; - const ctxProps: { value?: any; children: JSX.Element } = { + const ctxProps: { value?: any; children: Element } = { get children() { return fn(i + 1); }, @@ -119,7 +119,6 @@ export function MultiProvider(props if (Array.isArray(item)) { ctxProps.value = item[1]; item = item[0]; - if (typeof item !== "function") item = item.Provider; } return createComponent(item, ctxProps); diff --git a/packages/context/test/index.test.tsx b/packages/context/test/index.test.tsx index a60a16639..51dfaa40c 100644 --- a/packages/context/test/index.test.tsx +++ b/packages/context/test/index.test.tsx @@ -1,25 +1,24 @@ import { describe, test, expect } from "vitest"; import { createContext, - createRoot, type FlowComponent, - type JSX, + type Element, untrack, useContext, } from "solid-js"; -import { render } from "solid-js/web"; +import { render } from "@solidjs/web"; import { createContextProvider, MultiProvider } from "../src/index.js"; type TestContextValue = { message: string; - children: JSX.Element; + children: Element; }; const TEST_MESSAGE = "Hello, Context!"; const FALLBACK: TestContextValue = { message: "FALLBACK", children: undefined }; const [TestProvider, useTestContext] = createContextProvider( - (props: { text?: string; children: JSX.Element }): TestContextValue => { + (props: { text?: string; children: Element }): TestContextValue => { return { message: props.text ?? TEST_MESSAGE, get children() { @@ -94,23 +93,29 @@ describe("MultiProvider", () => { return {props.children}; }; - createRoot(() => { - - {untrack(() => { - runs++; - capture1 = useContext(Ctx1); - capture2 = useContext(Ctx2); - capture3 = useTestContext().message; - return ""; - })} - ; - }); + const container = document.createElement("div"); + const unmount = render( + () => ( + + {untrack(() => { + runs++; + capture1 = useContext(Ctx1); + capture2 = useContext(Ctx2); + capture3 = useTestContext().message; + return ""; + })} + + ), + container, + ); expect(runs).toBe(1); expect(capture1).toBe("Hello"); expect(capture2).toBe("World"); expect(capture3).toBe(TEST_MESSAGE); + + unmount(); }); }); diff --git a/packages/context/test/server.test.tsx b/packages/context/test/server.test.tsx index 8db05a5eb..b782e71b0 100644 --- a/packages/context/test/server.test.tsx +++ b/packages/context/test/server.test.tsx @@ -1,18 +1,18 @@ import { describe, test, expect } from "vitest"; -import { createContext, type FlowComponent, type JSX, untrack, useContext } from "solid-js"; -import { renderToString } from "solid-js/web"; +import { createContext, type FlowComponent, type Element, untrack, useContext } from "solid-js"; +import { renderToString } from "@solidjs/web"; import { createContextProvider, MultiProvider } from "../src/index.js"; type TestContextValue = { message: string; - children: JSX.Element; + children: Element; }; const TEST_MESSAGE = "Hello, Context!"; const FALLBACK: TestContextValue = { message: "FALLBACK", children: undefined }; const [TestProvider, useTestContext] = createContextProvider( - (props: { children: JSX.Element }): TestContextValue => { + (props: { children: Element }): TestContextValue => { return { message: TEST_MESSAGE, get children() { @@ -41,7 +41,7 @@ describe("MultiProvider", () => { renderToString(() => ( {untrack(() => { runs++; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7a083eac..ef360a457 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,9 +197,12 @@ importers: packages/context: devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13(solid-js@2.0.0-beta.13) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13 packages/controlled-props: dependencies: From 943027f76e99fd33fffd5c7b84560fd8a4e3b6cf Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Thu, 21 May 2026 10:40:19 -0400 Subject: [PATCH 2/2] Imprved context options, added new strict and layered primitive --- packages/context/CHANGELOG.md | 6 + packages/context/README.md | 64 ++++++++++- packages/context/package.json | 2 + packages/context/src/index.ts | 106 ++++++++++++++++- packages/context/test/index.test.tsx | 157 +++++++++++++++++++++++++- packages/context/test/server.test.tsx | 52 ++++++++- 6 files changed, 379 insertions(+), 8 deletions(-) diff --git a/packages/context/CHANGELOG.md b/packages/context/CHANGELOG.md index 681999f78..4ed19184e 100644 --- a/packages/context/CHANGELOG.md +++ b/packages/context/CHANGELOG.md @@ -14,6 +14,12 @@ - `JSX.Element` replaced with `Element` from `solid-js` throughout the public API types. - `MultiProvider` no longer falls back to accessing `.Provider` on non-function items — contexts passed in `values` must be functions (which all `Context` objects are in Solid 2.0). +#### New Exports + +- **`createStrictContextProvider`** — Like `createContextProvider` without defaults, but with the intent made explicit in types: the hook returns `T` (never `undefined`) and Solid throws `ContextNotFoundError` at runtime when used outside a provider. Accepts an optional `{ name }` option. +- **`createLayeredContext`** — Like `createContextProvider`, but each provider in the tree extends the parent context value rather than replacing it. The factory receives both `props` and the nearest parent context value. Requires a `defaults` base value. +- **`ContextProviderOptions`** — Exported type `{ name?: string }` for the third argument of `createContextProvider` and `createStrictContextProvider`/`createLayeredContext`'s second argument. + ## 0.3.2 ### Patch Changes diff --git a/packages/context/README.md b/packages/context/README.md index 7b21982ab..e5edcf396 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -11,6 +11,8 @@ Primitives simplifying the creation and use of SolidJS Context API. - [`createContextProvider`](#createcontextprovider) - Create the Context Provider component and useContext function with types inferred from the factory function. +- [`createStrictContextProvider`](#createstrictcontextprovider) - Like `createContextProvider`, but throws when the context is used outside a provider instead of returning `undefined`. +- [`createLayeredContext`](#createlayeredcontext) - Like `createContextProvider`, but each provider extends the parent context value rather than replacing it. - [`MultiProvider`](#multiprovider) - A component that allows you to provide multiple contexts at once. ## Installation @@ -74,16 +76,72 @@ const [CounterProvider, useCounter] = createContextProvider( const { count } = useCounter(); ``` -Definite context types without defaults: +### Debug name + +An optional `name` can be passed as part of the third argument. It labels the context's Symbol for Solid DevTools and improves `ContextNotFoundError` stack traces (dev mode only). ```ts -const useDefiniteCounter = () => useCounter()!; +const [ThemeProvider, useTheme] = createContextProvider( + () => createTheme(), + defaultTheme, + { name: "Theme" }, +); ``` ### Demo https://codesandbox.io/s/solid-primitives-context-demo-oqyie2?file=/index.tsx +## `createStrictContextProvider` + +Like `createContextProvider` without defaults, but with an explicit contract: if the hook is called outside a provider, Solid throws a `ContextNotFoundError` at runtime. The return type of the hook is `T` (never `undefined`), so no null-check is needed at the call site. + +```tsx +import { createStrictContextProvider } from "@solid-primitives/context"; + +const [AuthProvider, useAuth] = createStrictContextProvider( + () => { + const [user, setUser] = createSignal(null); + return { user, setUser }; + }, + { name: "Auth" }, +); + +// No `!` needed — type is T, not T | undefined +const { user } = useAuth(); +``` + +Use `createStrictContextProvider` when a context is required by contract (e.g. a form context that must be inside `

`). Use `createContextProvider` with defaults when a sensible fallback exists. + +## `createLayeredContext` + +Like `createContextProvider`, but each provider in the tree *extends* the parent context value rather than replacing it entirely. The factory function receives the nearest parent's context value as its second argument. + +This is useful for incremental overrides such as themes, permissions layers, or i18n patches where a child provider should inherit what it does not explicitly change. + +```tsx +import { createLayeredContext } from "@solid-primitives/context"; + +const [ThemeProvider, useTheme] = createLayeredContext( + (props: { primary?: string; secondary?: string }, parent) => ({ + ...parent, + primary: props.primary ?? parent.primary, + secondary: props.secondary ?? parent.secondary, + }), + { primary: "blue", secondary: "gray" }, // base defaults +); + +// Root: { primary: "red", secondary: "gray" } + + {/* Nested: { primary: "green", secondary: "gray" } — secondary inherited */} + + + +; +``` + +`createLayeredContext` always requires a `defaults` value (the base used when no parent provider wraps the component). The hook return type is always `T` (never `undefined`). + ## `MultiProvider` A component that allows you to provide multiple contexts at once. @@ -130,7 +188,7 @@ import { MultiProvider } from "@solid-primitives/context"; ``` > **Warning** -> Components and values passed to `MultiProvider` will be evaluated only once, so make sure that the structure is static. If is isn't, please use nested provider components instead. +> Components and values passed to `MultiProvider` will be evaluated only once, so make sure that the structure is static. If it isn't, please use nested provider components instead. ## Changelog diff --git a/packages/context/package.json b/packages/context/package.json index 9021e2f0f..57fb06e4f 100644 --- a/packages/context/package.json +++ b/packages/context/package.json @@ -17,6 +17,8 @@ "stage": 2, "list": [ "createContextProvider", + "createStrictContextProvider", + "createLayeredContext", "MultiProvider" ], "category": "Control Flow" diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 6a233daee..4119fdbad 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -15,10 +15,20 @@ export type ContextProvider = ( props: { children: Element } & T, ) => Element; +/** + * Options shared by context provider factories. + * @param name Debug label passed to the context's underlying Symbol — visible in Solid DevTools + * and `ContextNotFoundError` stack traces. Dev-mode only; stripped in production. + */ +export type ContextProviderOptions = { + name?: string; +}; + /** * Create the Context Provider component and useContext function with types inferred from the factory function. - * @param factoryFn Factory function will run when the provider component in executed. It takes the provider component `props` as it's argument, and what it returns will be available in the contexts for all the underlying components. - * @param defaults fallback returned from useContext function if the context wasn't provided + * @param factoryFn Factory function will run when the provider component is executed. It takes the provider component `props` as its argument, and what it returns will be available in the contexts for all the underlying components. + * @param defaults Fallback returned from useContext function if the context wasn't provided. + * @param options Optional configuration, including a debug `name` for DevTools. * @returns tuple of `[provider component, useContext function]` * @example * ```tsx @@ -39,6 +49,7 @@ export type ContextProvider = ( export function createContextProvider( factoryFn: (props: P) => T, defaults: T, + options?: ContextProviderOptions, ): [provider: ContextProvider

, useContext: () => T]; export function createContextProvider( factoryFn: (props: P) => T, @@ -46,8 +57,9 @@ export function createContextProvider( export function createContextProvider( factoryFn: (props: P) => T, defaults?: T, + options?: ContextProviderOptions, ): [provider: ContextProvider

, useContext: () => T | undefined] { - const ctx = createContext(defaults); + const ctx = createContext(defaults, options); return [ props => { return createComponent(ctx, { @@ -61,6 +73,94 @@ export function createContextProvider( ]; } +/** + * Like `createContextProvider`, but the returned hook always returns `T` — if the context is + * used outside a provider, Solid throws a `ContextNotFoundError` instead of returning + * `undefined`. Use this when the provider is required by contract. + * + * @param factoryFn Factory function run when the provider component mounts. Its return value + * becomes the context value available to all descendants. + * @param options Optional configuration, including a debug `name` for DevTools. + * @returns tuple of `[provider component, useContext function]` + * @example + * ```tsx + * const [AuthProvider, useAuth] = createStrictContextProvider(() => { + * const [user, setUser] = createSignal(null); + * return { user, setUser }; + * }, { name: "Auth" }); + * + * // throws if used outside + * const { user } = useAuth(); + * ``` + */ +export function createStrictContextProvider( + factoryFn: (props: P) => T, + options?: ContextProviderOptions, +): [provider: ContextProvider

, useContext: () => T] { + // No default value — getContext throws ContextNotFoundError when not inside a provider + const ctx = createContext(undefined, options); + return [ + props => { + return createComponent(ctx, { + value: factoryFn(props), + get children() { + return props.children; + }, + }); + }, + () => useContext(ctx), + ]; +} + +/** + * Like `createContextProvider`, but each provider in the tree *extends* the parent context + * value rather than replacing it. The factory receives the nearest parent's context value, + * letting it selectively override or merge properties. + * + * Useful for incremental overrides — themes, permissions, i18n patches — where a child + * provider should inherit what it does not explicitly override. + * + * @param factoryFn Factory called when the provider mounts. Receives the component `props` + * and the `parent` context value (or `defaults` at the root). Returns the new context value. + * @param defaults Base context value used when no parent provider is present. + * @param options Optional configuration, including a debug `name` for DevTools. + * @returns tuple of `[provider component, useContext function]` + * @example + * ```tsx + * const [ThemeProvider, useTheme] = createLayeredContext( + * (props: { primary?: string }, parent) => ({ ...parent, primary: props.primary ?? parent.primary }), + * { primary: "blue", secondary: "gray" }, + * ); + * + * // Root: { primary: "red", secondary: "gray" } + * + * // Nested: { primary: "green", secondary: "gray" } — secondary inherited + * + * + * + * + * ``` + */ +export function createLayeredContext( + factoryFn: (props: P, parent: T) => T, + defaults: T, + options?: ContextProviderOptions, +): [provider: ContextProvider

, useContext: () => T] { + const ctx = createContext(defaults, options); + return [ + props => { + const parent = useContext(ctx); + return createComponent(ctx, { + value: factoryFn(props, parent), + get children() { + return props.children; + }, + }); + }, + () => useContext(ctx), + ]; +} + /* MultiProvider inspired by the preact-multi-provider package from Marvin Hagemeister diff --git a/packages/context/test/index.test.tsx b/packages/context/test/index.test.tsx index 51dfaa40c..8376d01af 100644 --- a/packages/context/test/index.test.tsx +++ b/packages/context/test/index.test.tsx @@ -7,7 +7,12 @@ import { useContext, } from "solid-js"; import { render } from "@solidjs/web"; -import { createContextProvider, MultiProvider } from "../src/index.js"; +import { + createContextProvider, + createStrictContextProvider, + createLayeredContext, + MultiProvider, +} from "../src/index.js"; type TestContextValue = { message: string; @@ -75,6 +80,156 @@ describe("createContextProvider", () => { expect(ctx.children).toBe(TextComp); unmount(); }); + + test("accepts a debug name without affecting behavior", () => { + const [NamedProvider, useNamed] = createContextProvider(() => ({ value: 1 }), { value: 0 }, { + name: "Named", + }); + let captured = -1; + const unmount = render(() => { + captured = useNamed().value; + return ""; + }, document.createElement("div")); + expect(captured).toBe(0); + unmount(); + + const unmount2 = render( + () => {untrack(() => { captured = useNamed().value; return ""; })}, + document.createElement("div"), + ); + expect(captured).toBe(1); + unmount2(); + }); +}); + +describe("createStrictContextProvider", () => { + test("provides context value to descendants", () => { + const [StrictProvider, useStrict] = createStrictContextProvider( + (props: { value: number }) => ({ value: props.value }), + ); + + let captured = -1; + const container = document.createElement("div"); + const unmount = render( + () => ( + + {untrack(() => { + captured = useStrict().value; + return ""; + })} + + ), + container, + ); + expect(captured).toBe(42); + unmount(); + }); + + test("accepts a debug name", () => { + const [StrictProvider, useStrict] = createStrictContextProvider(() => ({ ok: true }), { + name: "Strict", + }); + let captured = false; + const unmount = render( + () => ( + + {untrack(() => { + captured = useStrict().ok; + return ""; + })} + + ), + document.createElement("div"), + ); + expect(captured).toBe(true); + unmount(); + }); + + test("throws ContextNotFoundError when used outside a provider", () => { + const [, useStrict] = createStrictContextProvider(() => ({ value: 1 })); + expect(() => { + render(() => { + useStrict(); + return ""; + }, document.createElement("div")); + }).toThrow(); + }); +}); + +describe("createLayeredContext", () => { + test("factory receives defaults as parent at the root level", () => { + const [LayeredProvider, useLayered] = createLayeredContext( + (props: { extra?: number }, parent) => ({ ...parent, extra: props.extra ?? parent.extra }), + { base: "root", extra: 0 }, + ); + + let captured: { base: string; extra: number } | undefined; + const unmount = render( + () => ( + + {untrack(() => { + captured = useLayered(); + return ""; + })} + + ), + document.createElement("div"), + ); + expect(captured?.base).toBe("root"); + expect(captured?.extra).toBe(7); + unmount(); + }); + + test("nested providers extend the parent context value", () => { + const [LayeredProvider, useLayered] = createLayeredContext( + (props: { level?: number; tag?: string }, parent) => ({ + ...parent, + level: props.level ?? parent.level, + tag: props.tag ?? parent.tag, + }), + { level: 0, tag: "default" }, + ); + + let capturedInner: { level: number; tag: string } | undefined; + let capturedOuter: { level: number; tag: string } | undefined; + + const unmount = render( + () => ( + + {untrack(() => { + capturedOuter = useLayered(); + }) as any} + + {untrack(() => { + capturedInner = useLayered(); + return ""; + })} + + + ), + document.createElement("div"), + ); + + expect(capturedOuter?.level).toBe(1); + expect(capturedOuter?.tag).toBe("outer"); + expect(capturedInner?.level).toBe(2); + expect(capturedInner?.tag).toBe("outer"); // inherited from outer provider + unmount(); + }); + + test("returns defaults when used outside any provider", () => { + const [, useLayered] = createLayeredContext( + (_props, parent) => parent, + { value: 99 }, + ); + let captured = -1; + const unmount = render(() => { + captured = useLayered().value; + return ""; + }, document.createElement("div")); + expect(captured).toBe(99); + unmount(); + }); }); describe("MultiProvider", () => { diff --git a/packages/context/test/server.test.tsx b/packages/context/test/server.test.tsx index b782e71b0..ffaa544aa 100644 --- a/packages/context/test/server.test.tsx +++ b/packages/context/test/server.test.tsx @@ -1,7 +1,12 @@ import { describe, test, expect } from "vitest"; import { createContext, type FlowComponent, type Element, untrack, useContext } from "solid-js"; import { renderToString } from "@solidjs/web"; -import { createContextProvider, MultiProvider } from "../src/index.js"; +import { + createContextProvider, + createStrictContextProvider, + createLayeredContext, + MultiProvider, +} from "../src/index.js"; type TestContextValue = { message: string; @@ -59,3 +64,48 @@ describe("MultiProvider", () => { expect(capture3).toBe(TEST_MESSAGE); }); }); + +describe("createStrictContextProvider (SSR)", () => { + test("provides context value to descendants", () => { + const [StrictProvider, useStrict] = createStrictContextProvider( + () => ({ greeting: "hello from server" }), + ); + let captured = ""; + renderToString(() => ( + + {untrack(() => { + captured = useStrict().greeting; + return captured; + })} + + )); + expect(captured).toBe("hello from server"); + }); +}); + +describe("createLayeredContext (SSR)", () => { + test("nested providers extend parent value", () => { + const [LayeredProvider, useLayered] = createLayeredContext( + (props: { level?: number }, parent) => ({ + ...parent, + level: props.level ?? parent.level, + }), + { level: 0, base: "ssr" }, + ); + + let capturedInner: { level: number; base: string } | undefined; + renderToString(() => ( + + + {untrack(() => { + capturedInner = useLayered(); + return ""; + })} + + + )); + + expect(capturedInner?.level).toBe(2); + expect(capturedInner?.base).toBe("ssr"); + }); +});