@kudzujs/core 0.8.8 → 0.8.10

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
@@ -8,9 +8,13 @@ HTML-first TSX framework with synchronous state semantics and no virtual DOM.
8
8
 
9
9
  Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTML, CSS, and only the route-specific ESM capabilities actually used. Static pages ship zero JavaScript. React, hydration, a VDOM, and a retained browser component tree are not part of the output.
10
10
 
11
+ [![Watch the 22-second Kudzu compiler overview](https://raw.githubusercontent.com/kudzujs/kudzu/main/media/kudzu-demo-cover.png)](https://github.com/kudzujs/kudzu/blob/main/media/kudzu-demo.mp4)
12
+
13
+ *Watch: React-shaped TSX to static HTML and route-specific ESM in 22 seconds.*
14
+
11
15
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
12
16
 
13
- **Latest release: 0.8.8 - Conditional keyed map roots.** Expression-bodied keyed maps may return `condition && <Row />` or `condition ? <Row /> : null`; Kudzu lowers the condition to its existing filter path so omitted rows clean up and re-entry starts fresh. Read the [release notes](./RELEASES.md#088---conditional-keyed-map-roots) or open the [release page](https://kudzujs.cloud/releases/0.8.8).
17
+ **Latest release: 0.8.10 - Native dialog migration.** A reduced shadcn/Radix-shaped dialog now proves how AI migration can preserve `forwardRef`, props, children, refs, and handlers while replacing package-owned Portal and Context behavior with native `<dialog>`. Read the [release notes](./RELEASES.md#0810---native-dialog-migration) or open the [release page](https://kudzujs.cloud/releases/0.8.10).
14
18
 
15
19
  - [Documentation](https://kudzujs.cloud/docs)
16
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
package/RELEASES.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.10 - Native dialog migration
4
+
5
+ Kudzu 0.8.10 proves a source migration path from a shadcn/Radix-shaped dialog to the native `<dialog>` element without adding package compatibility runtime.
6
+
7
+ ### New in 0.8.10
8
+
9
+ - A reduced migration fixture preserves a relative `forwardRef` component, props, children, object refs, and ordinary JSX event handlers.
10
+ - Package-owned Portal and Context behavior becomes native `showModal()`, `close()`, and cancel handling.
11
+ - Complete accessible dialog markup is pre-rendered while route output includes only the `click` and `cancel` events it uses.
12
+ - Chrome coverage verifies modal top-layer behavior, initial focus, confirm and cancel paths, and explicit trigger-focus restoration.
13
+ - Public documentation now includes the migration recipe and its deliberate library boundary.
14
+
15
+ ### Fixed in 0.8.10
16
+
17
+ - Mobile documentation code blocks stay within the content grid and scroll horizontally instead of widening the viewport.
18
+ - Browser tests allow slower CI Chrome startup and report process timeouts directly instead of failing later against empty DOM output.
19
+ - The complete suite passes 140/140 tests.
20
+
21
+ ### Boundary
22
+
23
+ This release does not execute Radix or arbitrary React UI packages. `Portal`, `asChild`/`Slot`, element cloning, and arbitrary compound-component Context must be removed during source migration. Applications remain responsible for dialog labeling and explicit focus restoration.
24
+
25
+ ### Upgrade
26
+
27
+ ```bash
28
+ npm install @kudzujs/core@^0.8.10
29
+ ```
30
+
31
+ ## 0.8.9 - Context-backed CRUD actions
32
+
33
+ Kudzu 0.8.9 specializes state-backed actions exposed through one conventional Context Provider and relative custom hook.
34
+
35
+ ### New in 0.8.9
36
+
37
+ - A relative zero-argument custom hook may directly return `useContext(ContextIdentifier)` from one local or named relative Context module.
38
+ - One Provider may expose direct shorthand `useState` pairs and synchronous actions that capture only those exposed pairs.
39
+ - Consumers may select state and actions across ordinary component boundaries; direct action calls inline into existing route handler ESM and concrete state operations.
40
+ - CRUD actions over object arrays compose with keyed rows, reactive selection, conditional editors, and guarded local storage effects in the React Notes migration.
41
+ - Unsupported private captures, hidden state dependencies, indirect action references, dynamic Provider values, multiple Providers, and consumer binding collisions fail with source diagnostics.
42
+ - Static sibling routes remain JavaScript-free. No action function, Context runtime, Provider tree, callback registry, VDOM, or hydration is emitted.
43
+ - The complete suite passes 139/139 tests, including Chrome coverage for create, rename, select, and delete behavior.
44
+
45
+ ### Boundary
46
+
47
+ The hook must directly return `useContext(ContextIdentifier)`. The Context and exactly one Provider must be declared together in one local or named relative module, and its value must be one direct shorthand object. Actions must be synchronous, called directly in intrinsic handlers, and capture only exposed Provider state pairs.
48
+
49
+ ### Upgrade
50
+
51
+ ```bash
52
+ npm install @kudzujs/core@^0.8.9
53
+ ```
54
+
3
55
  ## 0.8.8 - Conditional keyed map roots
4
56
 
5
57
  Kudzu 0.8.8 compiles ordinary expression-bodied conditional keyed maps through the existing pure collection selector path.
@@ -2843,6 +2843,78 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2843
2843
  let usesComponentRef = false
2844
2844
  let usesComponentEffects = false
2845
2845
 
2846
+ const resolveContextHook = (returned, hookSource) => {
2847
+ if (!hasFrameworkImport(hookSource, "useContext")) throw sourceNodeError(returned.expression, hookSource, "Relative Context hooks must call useContext imported from react or @kudzujs/core")
2848
+ if (returned.arguments.length !== 1 || !ts.isIdentifier(returned.arguments[0])) throw sourceNodeError(returned, hookSource, "Relative Context hooks must directly return useContext(ContextIdentifier)")
2849
+ const contextName = returned.arguments[0].text
2850
+ let providerSource = hookSource
2851
+ let providerContextName = contextName
2852
+ const hookImports = clientImportBindings(hookSource, hookSource.fileName, sourceFiles)
2853
+ if (hookImports.has(contextName)) {
2854
+ const binding = hookImports.get(contextName)
2855
+ if (binding.kind === "namespace" || binding.kind === "default") throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a named Context import")
2856
+ providerSource = importedSource(binding.target)
2857
+ providerContextName = binding.imported
2858
+ }
2859
+ const hasContext = hasFrameworkImport(providerSource, "createContext") && providerSource.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === providerContextName && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "createContext"))
2860
+ if (!hasContext) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a local or named relative createContext() declaration")
2861
+
2862
+ const providers = []
2863
+ const findProviders = node => {
2864
+ if (ts.isJsxAttribute(node) && node.name.text === "value") {
2865
+ const element = node.parent?.parent
2866
+ const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
2867
+ if (ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && tag.expression.text === providerContextName) providers.push(node)
2868
+ }
2869
+ ts.forEachChild(node, findProviders)
2870
+ }
2871
+ findProviders(providerSource)
2872
+ if (providers.length !== 1) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require exactly one Provider value in the Context module")
2873
+ const provider = providers[0]
2874
+ const value = provider.initializer && ts.isJsxExpression(provider.initializer) && provider.initializer.expression ? unwrapExpression(provider.initializer.expression) : undefined
2875
+ if (!value || !ts.isObjectLiteralExpression(value)) throw sourceNodeError(provider, providerSource, "Context Provider value must be one direct object literal")
2876
+ const owner = nearestFunction(provider)
2877
+ if (!owner) throw sourceNodeError(provider, providerSource, "Context Provider value must be returned by a component")
2878
+
2879
+ const states = new Map()
2880
+ const callbacks = new Map()
2881
+ const hasUseState = hasFrameworkImport(providerSource, "useState")
2882
+ const collectProviderBindings = node => {
2883
+ if (ts.isVariableDeclaration(node) && nearestFunction(node) === owner) {
2884
+ if (hasUseState && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "useState") {
2885
+ const [state, setter] = node.name.elements
2886
+ if (node.name.elements.length === 2 && state && setter && ts.isBindingElement(state) && ts.isBindingElement(setter) && ts.isIdentifier(state.name) && ts.isIdentifier(setter.name)) states.set(setter.name.text, state.name.text)
2887
+ }
2888
+ if (ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) callbacks.set(node.name.text, node.initializer)
2889
+ }
2890
+ ts.forEachChild(node, collectProviderBindings)
2891
+ }
2892
+ collectProviderBindings(owner.body)
2893
+
2894
+ const fields = new Set()
2895
+ const stateFields = new Set([...states].flat())
2896
+ for (const property of value.properties) {
2897
+ if (!ts.isShorthandPropertyAssignment(property)) throw sourceNodeError(property, providerSource, "Context Provider values must use direct shorthand state, setter, or action fields")
2898
+ const name = property.name.text
2899
+ if (!stateFields.has(name) && !callbacks.has(name)) throw sourceNodeError(property, providerSource, `Context Provider field ${JSON.stringify(name)} must be a direct provider-owned state, setter, or action`)
2900
+ fields.add(name)
2901
+ }
2902
+ for (const [setter, state] of states) {
2903
+ if (fields.has(setter) !== fields.has(state)) throw sourceNodeError(value, providerSource, `Context Provider state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be exposed together`)
2904
+ }
2905
+ for (const [name, callback] of callbacks) {
2906
+ if (!fields.has(name)) continue
2907
+ if (callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} must be synchronous`)
2908
+ const capture = nativeCaptureNames(callback, states).values().next().value
2909
+ if (capture) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
2910
+ for (const state of referencedStateNames(callback.body, states, callback)) {
2911
+ const setter = [...states].find(([, candidate]) => candidate === state)?.[0]
2912
+ if (!setter || !fields.has(state) || !fields.has(setter)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} requires exposed state and setter fields for ${JSON.stringify(state)}`)
2913
+ }
2914
+ }
2915
+ return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), states }
2916
+ }
2917
+
2846
2918
  const resolveCustomHook = (binding, call) => {
2847
2919
  const exportName = binding.kind === "default" ? "default" : binding.imported
2848
2920
  const key = `${binding.target}:${exportName}`
@@ -2852,7 +2924,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2852
2924
  if (hook.parameters.length || hook.asteriskToken || hook.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(hook.body)) throw sourceNodeError(hook, hookSource, "Relative custom hooks must be synchronous zero-argument functions with a block body")
2853
2925
  const returns = hook.body.statements.filter(ts.isReturnStatement)
2854
2926
  const returned = returns.length === 1 && returns[0] === hook.body.statements.at(-1) && returns[0].expression ? unwrapExpression(returns[0].expression) : undefined
2855
- if (!returned || !ts.isObjectLiteralExpression(returned)) throw sourceNodeError(hook.body, hookSource, "Relative custom hooks must end with one direct object return")
2927
+ if (returned && ts.isCallExpression(returned) && ts.isIdentifier(returned.expression) && returned.expression.text === "useContext") {
2928
+ const analysis = resolveContextHook(returned, hookSource)
2929
+ customHooks.set(key, analysis)
2930
+ return analysis
2931
+ }
2932
+ if (!returned || !ts.isObjectLiteralExpression(returned)) throw sourceNodeError(hook.body, hookSource, "Relative custom hooks must end with one direct object return or direct useContext(ContextIdentifier)")
2856
2933
 
2857
2934
  const states = new Map()
2858
2935
  const callbacks = new Map()
@@ -2897,7 +2974,30 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2897
2974
  const owner = nearestFunction(node)
2898
2975
  if (!owner) throw sourceNodeError(node, sourceFile, "Relative custom hooks cannot be used outside a Kudzu component")
2899
2976
  const setters = settersByFunction.get(owner) ?? new Map()
2977
+ const requiredContextStates = new Set()
2978
+ if (hook.context) {
2979
+ for (const name of names) {
2980
+ const callback = hook.callbacks.get(name)
2981
+ if (callback) for (const state of referencedStateNames(callback.body, hook.states, callback)) requiredContextStates.add(state)
2982
+ }
2983
+ }
2900
2984
  for (const [setter, state] of hook.states) {
2985
+ if (hook.context) {
2986
+ if (names.has(setter) && !names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative Context setter ${JSON.stringify(setter)} requires state ${JSON.stringify(state)} to be destructured`)
2987
+ if (!names.has(state) && !requiredContextStates.has(state)) continue
2988
+ setters.set(names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`, state)
2989
+ if (requiredContextStates.has(state)) {
2990
+ const fields = customHookPrivateFields.get(node) ?? []
2991
+ for (const field of [state, setter]) {
2992
+ if (names.has(field) || fields.includes(field)) continue
2993
+ const conflict = owner.parameters.some(parameter => bindingNames(parameter.name).includes(field)) || owner.body.statements.some(statement => statement !== node.parent.parent && statementDeclaresName(statement, field))
2994
+ if (conflict) throw sourceNodeError(node.name, sourceFile, `Context action state field ${JSON.stringify(field)} conflicts with a consumer binding`)
2995
+ fields.push(field)
2996
+ }
2997
+ customHookPrivateFields.set(node, fields)
2998
+ }
2999
+ continue
3000
+ }
2901
3001
  if (hook.privateStates.has(state)) {
2902
3002
  setters.set(setter, state)
2903
3003
  const fields = customHookPrivateFields.get(node) ?? []
@@ -2914,6 +3014,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2914
3014
  const callbacks = customHookFunctionsByOwner.get(owner) ?? new Map()
2915
3015
  callbacks.set(name, hook.callbacks.get(name))
2916
3016
  customHookFunctionsByOwner.set(owner, callbacks)
3017
+ if (hook.context) {
3018
+ const reducers = reducersByFunction.get(owner) ?? new Map()
3019
+ reducers.set(name, { contextAction: hook.callbacks.get(name), states: hook.states })
3020
+ reducersByFunction.set(owner, reducers)
3021
+ }
2917
3022
  }
2918
3023
  else if (![...hook.states].some(([setter, state]) => name === setter || name === state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook result ${JSON.stringify(name)} must be a direct useState value, setter, or callback`)
2919
3024
  }
@@ -5251,9 +5356,13 @@ function compileNativeCallback(expression, setters, reducers, factory, entries,
5251
5356
  const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
5252
5357
  const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
5253
5358
  imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
5254
- const captures = new Set([...allCaptures].filter(name => !importBindings.has(name)))
5359
+ const captures = new Set([...allCaptures].filter(name => !importBindings.has(name) && !usedReducers.has(name)))
5255
5360
  for (const entry of imports) if (!entry.package) clientImports.add(entry.target)
5256
5361
  const usedStates = nativeStateNames(expression, setters)
5362
+ for (const name of usedReducers) {
5363
+ const reducer = reducers.get(name)
5364
+ if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction)) usedStates.add(state)
5365
+ }
5257
5366
  const exportName = `${prefix}${entries.length}`
5258
5367
  entries.push({ exportName, expression, captures, imports, liveStates, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested })
5259
5368
  const value = name => deferValues
@@ -5572,6 +5681,14 @@ function clientImportBindings(sourceFile, file, sourceFiles) {
5572
5681
  return bindings
5573
5682
  }
5574
5683
 
5684
+ function hasFrameworkImport(sourceFile, name) {
5685
+ return sourceFile.statements.some(node => {
5686
+ if (!ts.isImportDeclaration(node) || node.importClause?.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !["react", "@kudzujs/core"].includes(node.moduleSpecifier.text)) return false
5687
+ const bindings = node.importClause?.namedBindings
5688
+ return bindings && ts.isNamedImports(bindings) && bindings.elements.some(entry => !entry.isTypeOnly && entry.name.text === name && (entry.propertyName ?? entry.name).text === name)
5689
+ })
5690
+ }
5691
+
5575
5692
  function packageImportBindings(sourceFile) {
5576
5693
  const bindings = new Map()
5577
5694
  const rejectDynamic = node => {
@@ -6136,15 +6253,23 @@ function printNativeHandler({ exportName, expression, captures, setters, reducer
6136
6253
  const visitor = node => {
6137
6254
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
6138
6255
  const reducer = reducers.get(node.expression.text)
6256
+ if (reducer.contextAction) {
6257
+ const action = synthesizeTree(cloneAst(reducer.contextAction, factory, context))
6258
+ const call = factory.createCallExpression(action, undefined, node.arguments)
6259
+ ts.setParentRecursive(call, false)
6260
+ return ts.visitNode(call, visitor)
6261
+ }
6139
6262
  if (reducer.store) return zustandActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
6140
6263
  if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
6141
6264
  return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
6142
6265
  }
6143
6266
  if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6267
+ if (reducers.get(node.name.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
6144
6268
  if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
6145
6269
  return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
6146
6270
  }
6147
6271
  if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6272
+ if (reducers.get(node.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
6148
6273
  if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
6149
6274
  return reducerReference(factory, reducers.get(node.text))
6150
6275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",