@kudzujs/core 0.7.13 → 0.7.14
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 +2 -2
- package/RELEASES.md +24 -0
- package/framework/README.md +1 -1
- package/framework/build.mjs +56 -1
- package/framework/core.mjs +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
10
10
|
|
|
11
11
|
> Experimental `0.7.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
|
-
**Latest release: 0.7.
|
|
13
|
+
**Latest release: 0.7.14 - Intrinsic forwardRef.** Ordinary React-shaped components can retain direct `forwardRef()` wrappers while Kudzu separates ref from props and erases the wrapper into intrinsic output. Read the [release notes](./RELEASES.md#0714---intrinsic-forwardref) or open the [release page](https://kudzujs.cloud/releases/0.7.14).
|
|
14
14
|
|
|
15
15
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
16
16
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
|
@@ -78,7 +78,7 @@ ordinary React-shaped TSX
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
- Function components execute at build time and do not survive as browser components.
|
|
81
|
-
- `useState` and reduced `useReducer` compile to synchronous logical state and batched direct DOM writes; top-level `useId`
|
|
81
|
+
- `useState` and reduced `useReducer` compile to synchronous logical state and batched direct DOM writes; top-level `useId` and direct intrinsic `forwardRef` authoring erase to static HTML without a component runtime.
|
|
82
82
|
- Conditions, keyed collections, attributes, events, refs, effects, and supported component boundaries compile to route-specific capabilities.
|
|
83
83
|
- Build-known data and routes become complete HTML through async components and `getStaticPaths()`.
|
|
84
84
|
- Native document navigation is the default; static routes do not load a client runtime.
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.14 - Intrinsic forwardRef
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.14 preserves conventional direct `forwardRef()` component authoring while erasing the wrapper into build-time intrinsic output.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.14
|
|
8
|
+
|
|
9
|
+
- `forwardRef` may be imported directly or with an alias from `react`, or called as a direct default/namespace React member.
|
|
10
|
+
- One top-level `const` component may wrap one inline synchronous `(props, ref)` render function.
|
|
11
|
+
- The compiler removes `ref` from ordinary props and rest bindings before supplying it as the render function's second parameter.
|
|
12
|
+
- The forwarded object ref must appear exactly once on the direct intrinsic root and reuses Kudzu's existing deterministic ref marker.
|
|
13
|
+
- Components remain valid when the optional ref prop is omitted; `null` and `undefined` intrinsic refs emit no marker.
|
|
14
|
+
- Same-file and relative-imported components compile without React, a wrapper function, hydration, or a browser component runtime.
|
|
15
|
+
- Indirect callbacks, async/generator renders, callback or composed refs, fragments, component roots, nested targets, repeated forwarding, and `memo(forwardRef(...))` fail with source diagnostics.
|
|
16
|
+
|
|
17
|
+
### Boundary
|
|
18
|
+
|
|
19
|
+
This release intentionally supports one direct object-ref boundary. It does not add React ref objects, imperative handles, generic ref composition, or browser component instances. Existing keyed-row ownership checks still require refs used inside keyed lists to originate from that row.
|
|
20
|
+
|
|
21
|
+
### Upgrade
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @kudzujs/core@^0.7.14
|
|
25
|
+
```
|
|
26
|
+
|
|
3
27
|
## 0.7.13 - Deterministic useId
|
|
4
28
|
|
|
5
29
|
Kudzu 0.7.13 preserves conventional top-level React `useId()` authoring while emitting deterministic static HTML IDs with no browser runtime.
|
package/framework/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Kudzu specializes ordinary common React-shaped TSX so migrations need minimal source restructuring. Declarative components, collection pipelines, conditions, hooks, and handlers should be lowered at build time rather than replaced with application-owned imperative DOM code. This principle applies across migrations and is not Stay-specific; it does not imply a React package, VDOM, hydration, or ecosystem runtime.
|
|
4
4
|
|
|
5
|
-
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, top-level `const` identifiers initialized by `useId()`, and default, namespace, or named `Fragment`. `useId()` becomes a deterministic build-time HTML ID and emits no browser capability; keyed rows reject it because cloned row templates cannot safely duplicate HTML IDs. Collection memos may start from local array state or a named relative import of an exported JSON-safe `const` array and may read direct local state declared in their dependency array. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined into existing bindings and keyed-list selectors because no browser component rerender or memo cache exists. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
5
|
+
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, direct intrinsic `forwardRef`, top-level `const` identifiers initialized by `useId()`, and default, namespace, or named `Fragment`. `forwardRef()` accepts one inline synchronous `(props, ref)` function and requires the object ref exactly once on its direct intrinsic root; the compiler removes `ref` from props/rest and erases the wrapper. `useId()` becomes a deterministic build-time HTML ID and emits no browser capability; keyed rows reject it because cloned row templates cannot safely duplicate HTML IDs. Collection memos may start from local array state or a named relative import of an exported JSON-safe `const` array and may read direct local state declared in their dependency array. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined into existing bindings and keyed-list selectors because no browser component rerender or memo cache exists. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
6
6
|
|
|
7
7
|
Direct `clsx` calls over literal strings, numbers, arrays, object conditions, and conditional expressions are similarly lowered to ordinary concatenation and conditional expressions. The package import is erased, and dynamic classes continue through the existing binding compiler without serializing or shipping the `clsx` function.
|
|
8
8
|
|
package/framework/build.mjs
CHANGED
|
@@ -1883,7 +1883,7 @@ function normalizeZustandMigrationSyntax(sourceFile, factory, context) {
|
|
|
1883
1883
|
|
|
1884
1884
|
function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections = new Set()) {
|
|
1885
1885
|
const supported = new Set(["createContext", "useContext", "useEffect", "useId", "useReducer", "useRef", "useState"])
|
|
1886
|
-
const erased = new Set(["memo", "useCallback", "useMemo"])
|
|
1886
|
+
const erased = new Set(["forwardRef", "memo", "useCallback", "useMemo"])
|
|
1887
1887
|
const aliases = new Map()
|
|
1888
1888
|
const reactObjects = new Set()
|
|
1889
1889
|
for (const statement of sourceFile.statements) {
|
|
@@ -1988,6 +1988,7 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCol
|
|
|
1988
1988
|
}
|
|
1989
1989
|
if (ts.isCallExpression(node)) {
|
|
1990
1990
|
const name = migrationCallName(node)
|
|
1991
|
+
if (name === "forwardRef") return ts.visitNode(lowerReactForwardRef(node, sourceFile, factory), visitor)
|
|
1991
1992
|
if (name === "memo") {
|
|
1992
1993
|
if (node.arguments.length !== 1 || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]) || ts.isIdentifier(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React memo() requires exactly one function component or component identifier")
|
|
1993
1994
|
if (ts.isIdentifier(node.arguments[0]) && isShadowedIdentifier(node.arguments[0], sourceFile)) throw sourceNodeError(node.arguments[0], sourceFile, "React memo() component identifiers must resolve to an unshadowed same-file top-level function")
|
|
@@ -2074,6 +2075,60 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCol
|
|
|
2074
2075
|
return normalized
|
|
2075
2076
|
}
|
|
2076
2077
|
|
|
2078
|
+
function lowerReactForwardRef(call, sourceFile, factory) {
|
|
2079
|
+
const declaration = call.parent
|
|
2080
|
+
const statement = declaration?.parent?.parent
|
|
2081
|
+
if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== call || !ts.isIdentifier(declaration.name) || !statement || !ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.parent !== sourceFile) {
|
|
2082
|
+
throw sourceNodeError(call, sourceFile, "React forwardRef() must directly initialize one top-level const component")
|
|
2083
|
+
}
|
|
2084
|
+
if (call.arguments.length !== 1 || !ts.isArrowFunction(call.arguments[0]) && !ts.isFunctionExpression(call.arguments[0])) throw sourceNodeError(call, sourceFile, "React forwardRef() requires exactly one inline render function")
|
|
2085
|
+
const callback = call.arguments[0]
|
|
2086
|
+
if (callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, sourceFile, "React forwardRef() render function must be synchronous and cannot be a generator")
|
|
2087
|
+
if (callback.parameters.length !== 2) throw sourceNodeError(callback, sourceFile, "React forwardRef() render function must declare exactly (props, ref)")
|
|
2088
|
+
const [props, ref] = callback.parameters
|
|
2089
|
+
if (props.dotDotDotToken || props.initializer || !ts.isIdentifier(props.name) && !ts.isObjectBindingPattern(props.name)) throw sourceNodeError(props, sourceFile, "React forwardRef() props must use one identifier or a flat object binding")
|
|
2090
|
+
if (ref.dotDotDotToken || ref.initializer || !ts.isIdentifier(ref.name)) throw sourceNodeError(ref, sourceFile, "React forwardRef() ref parameter must be one identifier")
|
|
2091
|
+
|
|
2092
|
+
let elements
|
|
2093
|
+
if (ts.isIdentifier(props.name)) {
|
|
2094
|
+
elements = [
|
|
2095
|
+
factory.createBindingElement(undefined, factory.createIdentifier("ref"), factory.createIdentifier(ref.name.text)),
|
|
2096
|
+
factory.createBindingElement(factory.createToken(ts.SyntaxKind.DotDotDotToken), undefined, factory.createIdentifier(props.name.text))
|
|
2097
|
+
]
|
|
2098
|
+
} else {
|
|
2099
|
+
for (const element of props.name.elements) {
|
|
2100
|
+
const property = (element.propertyName ?? element.name)
|
|
2101
|
+
if (!ts.isIdentifier(element.name) || property.text === "ref") throw sourceNodeError(element, sourceFile, property.text === "ref" ? "React forwardRef() props must not declare ref; Kudzu supplies ref through the second parameter" : "React forwardRef() props must use one identifier or a flat object binding")
|
|
2102
|
+
}
|
|
2103
|
+
const rest = props.name.elements.findIndex(element => Boolean(element.dotDotDotToken))
|
|
2104
|
+
elements = [...props.name.elements]
|
|
2105
|
+
elements.splice(rest < 0 ? elements.length : rest, 0, factory.createBindingElement(undefined, factory.createIdentifier("ref"), factory.createIdentifier(ref.name.text)))
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
const last = ts.isBlock(callback.body) ? callback.body.statements.at(-1) : undefined
|
|
2109
|
+
let returnCount = 0
|
|
2110
|
+
const countReturns = node => {
|
|
2111
|
+
if (node !== callback.body && isFunctionLike(node)) return
|
|
2112
|
+
if (ts.isReturnStatement(node)) returnCount++
|
|
2113
|
+
ts.forEachChild(node, countReturns)
|
|
2114
|
+
}
|
|
2115
|
+
countReturns(callback.body)
|
|
2116
|
+
const returned = ts.isBlock(callback.body)
|
|
2117
|
+
? last && ts.isReturnStatement(last) ? last.expression : undefined
|
|
2118
|
+
: callback.body
|
|
2119
|
+
const root = returned && unwrapExpression(returned)
|
|
2120
|
+
const tag = root && jsxTagName(root)
|
|
2121
|
+
if ((ts.isBlock(callback.body) && returnCount !== 1) || !root || !ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root) || !ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) throw sourceNodeError(callback.body, sourceFile, "React forwardRef() render function must directly return one intrinsic JSX element")
|
|
2122
|
+
const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
2123
|
+
const forwarded = attributes.properties.filter(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "ref" && ts.isJsxExpression(attribute.initializer) && ts.isIdentifier(attribute.initializer.expression) && attribute.initializer.expression.text === ref.name.text)
|
|
2124
|
+
if (forwarded.length !== 1 || referenceIdentifiers(callback.body, ref.name.text).length !== 1) throw sourceNodeError(ref, sourceFile, "React forwardRef() ref must be forwarded exactly once as ref={ref} on the direct intrinsic root")
|
|
2125
|
+
|
|
2126
|
+
const parameter = factory.updateParameterDeclaration(props, props.modifiers, undefined, factory.createObjectBindingPattern(elements), props.questionToken, props.type, undefined)
|
|
2127
|
+
return ts.isArrowFunction(callback)
|
|
2128
|
+
? factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, [parameter], callback.type, callback.equalsGreaterThanToken, callback.body)
|
|
2129
|
+
: factory.updateFunctionExpression(callback, callback.modifiers, undefined, callback.name, callback.typeParameters, [parameter], callback.type, callback.body)
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2077
2132
|
function validateUseIdSyntax(sourceFile) {
|
|
2078
2133
|
const imported = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useId"))
|
|
2079
2134
|
if (!imported) return
|
package/framework/core.mjs
CHANGED
|
@@ -708,6 +708,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
708
708
|
for (const [rawName, value] of Object.entries(props)) {
|
|
709
709
|
if (rawName === "children" || rawName === "key") continue
|
|
710
710
|
if (rawName === "ref") {
|
|
711
|
+
if (value == null) continue
|
|
711
712
|
if (!value?.[refMarker]) throw new Error("ref must be created by useRef(null)")
|
|
712
713
|
if (renderContext.listDepth && !value.row) throw new Error("Refs in keyed lists must be declared by the keyed row component")
|
|
713
714
|
attributes += ` data-k-ref="${value.id}"`
|