@kudzujs/core 0.4.11 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -139,6 +139,26 @@ return <>
139
139
 
140
140
  Kudzu resolves `current` when the handler reads it, so removed conditional elements return `null` without a component runtime. Refs must initialize with `null`; callback refs, mutable value refs, and refs inside keyed lists are not supported.
141
141
 
142
+ ## Context
143
+
144
+ Create a context to pass static or reactive values through component layers without prop drilling:
145
+
146
+ ```tsx
147
+ const ThemeContext = createContext("light")
148
+
149
+ function Toolbar() {
150
+ const theme = useContext(ThemeContext)
151
+ return <button className={`theme-${theme}`}>{theme}</button>
152
+ }
153
+
154
+ function App() {
155
+ const [theme] = useState("dark")
156
+ return <ThemeContext.Provider value={theme}><Toolbar /></ThemeContext.Provider>
157
+ }
158
+ ```
159
+
160
+ The default value applies outside a Provider, nested Providers override their parent, and native handlers read the latest direct state value. Static Provider values must be serializable. Reactive Provider values must be a direct `useState` value; setters and objects containing reactive values are not supported.
161
+
142
162
  ## Conditional DOM
143
163
 
144
164
  Inline child `&&` and ternary expressions insert and remove bounded DOM ranges directly. A menu bar needs only state setters:
@@ -261,6 +281,7 @@ Supported:
261
281
  - Reactive standard, `aria-*`, and `data-*` attributes
262
282
  - Reactive object `style` attributes
263
283
  - Object DOM refs in native event handlers
284
+ - Default, nested, and reactive context providers
264
285
  - Controlled `value` and `checked` form properties
265
286
  - Conditional child `&&` and ternary DOM patches
266
287
  - Top-level immutable JSX locals
@@ -283,13 +304,13 @@ Same counter with initial value `7` and increment/decrement buttons:
283
304
 
284
305
  | Framework | Initial content | Initial JS gzip | Total output | Clean build |
285
306
  |---|---:|---:|---:|---:|
286
- | Kudzu | Yes | 393 B | 1.1 KB | **429 ms** |
287
- | Astro | Yes | **158 B** | **365 B** | 942 ms |
288
- | Svelte CSR | No | 10.5 KB | 26.9 KB | 850 ms |
289
- | Qwik CSR | No | 20.6 KB | 57.8 KB | 686 ms |
290
- | Vue CSR | No | 24.0 KB | 60.3 KB | 875 ms |
291
- | React CSR | No | 59.2 KB | 189.0 KB | 1150 ms |
292
- | Next.js | Yes | 182.1 KB | 652.2 KB | 3203 ms |
307
+ | Kudzu | Yes | 393 B | 1.1 KB | **409 ms** |
308
+ | Astro | Yes | **158 B** | **365 B** | 893 ms |
309
+ | Svelte CSR | No | 10.5 KB | 26.9 KB | 867 ms |
310
+ | Qwik CSR | No | 20.6 KB | 57.8 KB | 600 ms |
311
+ | Vue CSR | No | 24.0 KB | 60.3 KB | 785 ms |
312
+ | React CSR | No | 59.2 KB | 189.0 KB | 1032 ms |
313
+ | Next.js | Yes | 182.1 KB | 652.2 KB | 3082 ms |
293
314
 
294
315
  Astro produces the smallest hand-authored counter. Kudzu's advantage in this fixture is React-shaped state code with a sub-1 KB runtime, not the smallest possible JavaScript.
295
316
 
@@ -299,13 +320,13 @@ Same content and CSS across every fixture:
299
320
 
300
321
  | Framework | Initial content | Initial JS gzip | Total output | Clean build |
301
322
  |---|---:|---:|---:|---:|
302
- | Kudzu | Yes | **0 B** | 3.2 KB | **395 ms** |
303
- | Astro | Yes | **0 B** | **3.0 KB** | 1048 ms |
304
- | Svelte CSR | No | 10.2 KB | 27.2 KB | 897 ms |
305
- | Qwik CSR | No | 20.2 KB | 59.6 KB | 608 ms |
306
- | Vue CSR | No | 24.2 KB | 62.3 KB | 801 ms |
307
- | React CSR | No | 59.8 KB | 192.3 KB | 1097 ms |
308
- | Next.js | Yes | 182.6 KB | 663.6 KB | 3085 ms |
323
+ | Kudzu | Yes | **0 B** | 3.2 KB | **422 ms** |
324
+ | Astro | Yes | **0 B** | **3.0 KB** | 1081 ms |
325
+ | Svelte CSR | No | 10.2 KB | 27.2 KB | 902 ms |
326
+ | Qwik CSR | No | 20.2 KB | 59.6 KB | 633 ms |
327
+ | Vue CSR | No | 24.2 KB | 62.3 KB | 810 ms |
328
+ | React CSR | No | 59.8 KB | 192.3 KB | 1110 ms |
329
+ | Next.js | Yes | 182.6 KB | 663.6 KB | 3126 ms |
309
330
 
310
331
  ### 1,000-item Keyed List
311
332
 
@@ -313,15 +334,15 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
313
334
 
314
335
  | Framework | Initial content | Initial JS gzip | Total output | Build | Update | Reverse | Remove | Add | Operations total |
315
336
  |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
316
- | Astro | Yes | **324 B** | **43.6 KB** | 843 ms | **4.7 ms** | **4.4 ms** | **1.6 ms** | **3.6 ms** | **14.3 ms** |
317
- | Kudzu | Yes | 5.0 KB | 60.3 KB | **432 ms** | 8.0 ms | 8.0 ms | 2.1 ms | 7.8 ms | 25.9 ms |
318
- | Vue CSR | No | 24.3 KB | 61.3 KB | 776 ms | 12.0 ms | 10.8 ms | 4.6 ms | 7.4 ms | 34.8 ms |
319
- | React CSR | No | 59.3 KB | 189.4 KB | 1032 ms | 11.9 ms | 14.5 ms | 4.7 ms | 6.5 ms | 37.6 ms |
320
- | Next.js | Yes | 182.2 KB | 695.2 KB | 2988 ms | 8.6 ms | 15.8 ms | 5.1 ms | 8.5 ms | 38.0 ms |
321
- | Svelte CSR | No | 12.9 KB | 33.1 KB | 858 ms | 6.6 ms | 48.5 ms | 5.2 ms | 7.3 ms | 67.6 ms |
322
- | Qwik CSR | No | 22.2 KB | 64.1 KB | 618 ms | 11.4 ms | 27.5 ms | 37.9 ms | 22.8 ms | 99.6 ms |
323
-
324
- Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 25.9 ms, 11.6 ms behind the hand-authored Astro baseline and 11.7 ms ahead of React across all four operations.
337
+ | Astro | Yes | **324 B** | **43.6 KB** | 826 ms | **4.1 ms** | **3.7 ms** | **1.4 ms** | **3.3 ms** | **12.5 ms** |
338
+ | Kudzu | Yes | 5.0 KB | 60.3 KB | **452 ms** | 7.1 ms | 7.5 ms | 1.9 ms | 7.0 ms | 23.5 ms |
339
+ | Next.js | Yes | 182.2 KB | 695.2 KB | 3002 ms | 7.5 ms | 12.3 ms | 4.1 ms | 7.8 ms | 31.7 ms |
340
+ | Vue CSR | No | 24.3 KB | 61.3 KB | 765 ms | 11.4 ms | 9.8 ms | 4.4 ms | 7.0 ms | 32.6 ms |
341
+ | React CSR | No | 59.3 KB | 189.4 KB | 1039 ms | 9.9 ms | 13.4 ms | 4.7 ms | 6.1 ms | 34.1 ms |
342
+ | Svelte CSR | No | 12.9 KB | 33.1 KB | 845 ms | 6.2 ms | 42.9 ms | 4.6 ms | 6.2 ms | 59.9 ms |
343
+ | Qwik CSR | No | 22.2 KB | 64.1 KB | 630 ms | 10.7 ms | 27.5 ms | 39.2 ms | 22.2 ms | 99.6 ms |
344
+
345
+ Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 23.5 ms, 11.0 ms behind the hand-authored Astro baseline and 10.6 ms ahead of React across all four operations.
325
346
 
326
347
  Benchmark snapshot collected on July 22, 2026 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
327
348
 
@@ -1,7 +1,7 @@
1
1
  # Framework Internals
2
2
 
3
3
  - `build.mjs`: TSX compilation, file routes, behavior extraction, static HTML output, and the development server.
4
- - `core.mjs`: server-side JSX rendering, state slots, behavior metadata, and serializable capture validation.
4
+ - `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
5
5
  - `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
6
6
  - `runtime.js`: command-only runtime for direct state-to-text patches.
7
7
  - `shared-runtime.js`: command runtime with capability commit and DOM lifecycle hooks, emitted only when needed.
@@ -8,6 +8,13 @@ export interface RefObject<T> {
8
8
 
9
9
  export function useRef<T>(initialValue: null): RefObject<T>
10
10
 
11
+ export interface Context<T> {
12
+ Provider: (props: { value: T; children?: unknown }) => unknown
13
+ }
14
+
15
+ export function createContext<T>(defaultValue: T): Context<T>
16
+ export function useContext<T>(context: Context<T>): T
17
+
11
18
  export function behavior(commands: Array<["add" | "set" | "log", unknown, unknown]>): unknown
12
19
  export function nativeBehavior(module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
13
20
  export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
@@ -10,6 +10,8 @@ const listFieldMarker = Symbol("kudzu.listField")
10
10
  const listExpressionMarker = Symbol("kudzu.listExpression")
11
11
  const listItemMarker = Symbol("kudzu.listItem")
12
12
  const refMarker = Symbol("kudzu.ref")
13
+ const contextMarker = Symbol("kudzu.context")
14
+ const contextProviderMarker = Symbol("kudzu.contextProvider")
13
15
  const noSelectValue = Symbol("kudzu.no-select-value")
14
16
 
15
17
  let renderContext
@@ -44,6 +46,23 @@ export function useRef(initialValue) {
44
46
  return { [refMarker]: true, id: `r${renderContext.nextRef++}`, current: null }
45
47
  }
46
48
 
49
+ export function createContext(defaultValue) {
50
+ const context = { [contextMarker]: true, defaultValue }
51
+ context.Provider = function Provider({ value, children }) {
52
+ return { [contextProviderMarker]: true, context, value, children }
53
+ }
54
+ return context
55
+ }
56
+
57
+ export function useContext(context) {
58
+ if (!renderContext) throw new Error("useContext() can only run while rendering a Kudzu component")
59
+ if (!context?.[contextMarker]) throw new Error("useContext() requires a Kudzu context")
60
+ for (let index = renderContext.contexts.length - 1; index >= 0; index--) {
61
+ if (renderContext.contexts[index][0] === context) return renderContext.contexts[index][1]
62
+ }
63
+ return context.defaultValue
64
+ }
65
+
47
66
  export function behavior(commands) {
48
67
  return {
49
68
  [behaviorMarker]: true,
@@ -63,7 +82,7 @@ export function nativeBehavior(module, handler, states, scope) {
63
82
  if (!signal?.[signalMarker]) throw new Error("A native behavior must target framework state")
64
83
  return [name, signal.id]
65
84
  })),
66
- scope: Object.fromEntries(scope.map(([name, value]) => [name, serializeCapture(name, value, new Set())]))
85
+ scope: Object.fromEntries(scope.map(([name, value]) => [name, value?.[signalMarker] ? { type: "state", id: value.id } : serializeCapture(name, value, new Set())]))
67
86
  }
68
87
  }
69
88
 
@@ -197,7 +216,7 @@ function serializeCapture(name, value, seen) {
197
216
  }
198
217
 
199
218
  export async function renderPage(component, metadata = {}) {
200
- renderContext = { nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listFields: undefined, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false, hasListStyles: false }
219
+ renderContext = { nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listFields: undefined, contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false, hasListStyles: false }
201
220
 
202
221
  try {
203
222
  const body = await renderNode({ type: component, props: {} })
@@ -299,6 +318,14 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
299
318
  return escapeHtml(node)
300
319
  }
301
320
  if (node instanceof Promise) return renderNode(await node, namespace, selectValue)
321
+ if (node?.[contextProviderMarker]) {
322
+ renderContext.contexts.push([node.context, node.value])
323
+ try {
324
+ return await renderNode(node.children, namespace, selectValue)
325
+ } finally {
326
+ renderContext.contexts.pop()
327
+ }
328
+ }
302
329
  if (node?.[conditionalMarker]) {
303
330
  const descriptor = bindingDescriptor(node)
304
331
  const stateIds = reactiveStateIds(descriptor)
@@ -20,7 +20,7 @@ export function createNativeContext(state, stateIds, commit, serializedScope = {
20
20
  return state.get(stateIds[name])
21
21
  },
22
22
  scope(name) {
23
- return scope[name]
23
+ return serializedScope[name]?.type === "state" ? state.get(serializedScope[name].id) : scope[name]
24
24
  },
25
25
  set(name, value) {
26
26
  const id = stateIds[name]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",