@kudzujs/core 0.7.3 → 0.7.4

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
@@ -10,7 +10,7 @@ Kudzu is designed so ordinary common React-shaped TSX can migrate with minimal s
10
10
 
11
11
  > Experimental `0.7.x`: the compiler API and supported TSX surface may change.
12
12
 
13
- **0.7.3:** React memo normalization. Same-file `memo`, inline `useCallback`, and direct-state expression `useMemo` now compile to existing Kudzu capabilities without React, a VDOM, or hydration. See [release notes](./RELEASES.md#073---react-memo-normalization).
13
+ **0.7.4:** Memoized collection pipelines. React `useMemo` may retain analyzable `filter`, `map`, `flatMap`, and `Array.from` pipelines while compiling to existing keyed-list selectors without React, a VDOM, or hydration. See [release notes](./RELEASES.md#074---memoized-collection-pipelines).
14
14
 
15
15
  Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
16
16
 
@@ -74,7 +74,7 @@ export default function Header() {
74
74
  }
75
75
  ```
76
76
 
77
- Kudzu rewrites supported React imports to its compile-time APIs before evaluating the module; neither the React package nor a compatibility runtime enters the deploy output. Named or aliased `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports compile to their canonical forms. Default and namespace imports may call those APIs as direct members such as `React.useState`, and default, namespace, or named `Fragment` also works. `memo(Component)` is erased to a same-file component. Inline `useCallback(function, literalDependencies)` is erased to its function, while inline synchronous `useMemo` callbacks returning one expression over primitive literals and direct local state are inlined at same-component uses so existing bindings track that state. Both hooks require inert literal dependency arrays and complete captured-state dependencies; memo locals cannot be duplicated or captured by nested functions. React classes and side-effect or dynamic React imports remain unsupported. A static route using these forms still emits zero JavaScript.
77
+ Kudzu rewrites supported React imports to its compile-time APIs before evaluating the module; neither the React package nor a compatibility runtime enters the deploy output. Named or aliased `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports compile to their canonical forms. Default and namespace imports may call those APIs as direct members such as `React.useState`, and default, namespace, or named `Fragment` also works. `memo(Component)` is erased to a same-file component. Inline `useCallback(function, literalDependencies)` is erased to its function, while inline synchronous `useMemo` callbacks may return one expression over primitive literals/direct local state or an analyzable `filter`, `map`, `flatMap`, and `Array.from` collection pipeline. Scalar expressions inline into existing bindings; collection pipelines lower to existing keyed-list selectors and preserve row identity. Both hooks require inert literal dependency arrays and complete captured-state dependencies; memo locals cannot be duplicated or captured by nested functions. React classes and side-effect or dynamic React imports remain unsupported. A static route using these forms still emits zero JavaScript.
78
78
 
79
79
  Create `src/pages/index.tsx`:
80
80
 
package/RELEASES.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.7.4 - Memoized collection pipelines
4
+
5
+ Kudzu 0.7.4 lets ordinary React/Vite source retain analyzable collection work inside `useMemo` while reusing the existing keyed-list selector and DOM identity model.
6
+
7
+ ### New in 0.7.4
8
+
9
+ - Inline `useMemo` callbacks accept collection pipelines rooted in direct local array state.
10
+ - Existing `filter`, direct-property `flatMap`, and `Array.from` selector analysis is reused without a browser memo cache.
11
+ - Intermediate `.map()` calls lower to the existing `Array.from(source, mapper)` selector operation.
12
+ - Memo locals are removed from emitted server modules after their uses are inlined, so state signals never execute array methods during rendering.
13
+ - State updates preserve keyed row DOM identity while adding, removing, filtering, mapping, and reordering selected values.
14
+ - Collection callbacks must be synchronous arrows with plain `(item)` or `(item, index)` identifier parameters; async, rest, default, and optional parameters fail with source locations.
15
+ - The React/Vite fixture verifies `filter + map`, selector generation, browser updates, and retained identity for an existing row.
16
+
17
+ ### Boundary
18
+
19
+ Memoized collections use the existing statically analyzable collection subset. Arbitrary callbacks, external captures, asynchronous transforms, getters, and general-purpose memo caching remain unsupported. React, a VDOM, hydration, and a retained browser component tree are not emitted.
20
+
21
+ ### Upgrade
22
+
23
+ ```bash
24
+ npm install @kudzujs/core@^0.7.4
25
+ ```
26
+
3
27
  ## 0.7.3 - React memo normalization
4
28
 
5
29
  Kudzu 0.7.3 accepts common React memo authoring forms while preserving build-time components, direct state bindings, static HTML, and capability-only JavaScript.
@@ -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 `useMemo`, and default, namespace, or named `Fragment`. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined 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`, and default, namespace, or named `Fragment`. `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
  - `build.mjs`: TSX compilation, static, `getStaticPaths`, and runtime-fallback routes, base paths, CSS collection, post-build hooks, behavior extraction, static HTML output, and the development server.
8
8
  - `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
@@ -1799,7 +1799,7 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
1799
1799
  if (!isLocalConst(node)) throw sourceNodeError(node, sourceFile, "React useMemo() local values must use const declarations")
1800
1800
  const callback = node.initializer.arguments[0]
1801
1801
  if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
1802
- const expression = reactMemoExpression(callback)
1802
+ const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
1803
1803
  const owner = nearestFunction(node)
1804
1804
  if (owner && expression) {
1805
1805
  const entries = memoLocals.get(owner) ?? new Map()
@@ -1835,6 +1835,15 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
1835
1835
  const required = new Set()
1836
1836
  const imported = new Set()
1837
1837
  const visitor = node => {
1838
+ if (ts.isVariableStatement(node)) {
1839
+ const entries = memoLocals.get(nearestFunction(node))
1840
+ if (entries) {
1841
+ for (const declaration of node.declarationList.declarations) if (ts.isIdentifier(declaration.name) && entries.has(declaration.name.text) && declaration.initializer) ts.visitNode(declaration.initializer, visitor)
1842
+ const declarations = node.declarationList.declarations.filter(declaration => !ts.isIdentifier(declaration.name) || !entries.has(declaration.name.text))
1843
+ if (!declarations.length) return undefined
1844
+ if (declarations.length !== node.declarationList.declarations.length) return factory.updateVariableStatement(node, node.modifiers, factory.updateVariableDeclarationList(node.declarationList, declarations.map(declaration => ts.visitEachChild(declaration, visitor, context))))
1845
+ }
1846
+ }
1838
1847
  if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
1839
1848
  const owner = nearestFunctionLike(node)
1840
1849
  const entry = memoLocals.get(owner)?.get(node.text)
@@ -1865,14 +1874,19 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
1865
1874
  if (callback.parameters.length || callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, sourceFile, "React useMemo() callback must be synchronous and parameterless")
1866
1875
  const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
1867
1876
  if (dependency) throw sourceNodeError(dependency, sourceFile, "React useMemo() dependencies must be identifiers or primitive literals")
1868
- const expression = reactMemoExpression(callback)
1869
- if (!expression || !isPureReactMemoExpression(expression)) throw sourceNodeError(callback.body, sourceFile, "React useMemo() callback must return one pure expression")
1877
+ const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
1870
1878
  const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
1871
1879
  const owner = nearestFunction(node)
1872
1880
  const states = owner ? ownerStateNames(owner) : new Set()
1873
- const unsupported = [...reactMemoReferenceNames(expression)].find(reference => !states.has(reference))
1874
- if (unsupported) throw sourceNodeError(expression, sourceFile, `React useMemo() pure expressions may only reference direct local state; found ${JSON.stringify(unsupported)}`)
1875
- const stale = [...states].find(state => referenceIdentifiers(expression, state).length && !dependencies.has(state))
1881
+ const collectionState = expression && reactMemoCollectionState(expression, states, sourceFile)
1882
+ if (!expression || !collectionState && !isPureReactMemoExpression(expression)) throw sourceNodeError(callback.body, sourceFile, "React useMemo() callback must return one pure expression or analyzable collection pipeline")
1883
+ if (!collectionState) {
1884
+ const unsupported = [...reactMemoReferenceNames(expression)].find(reference => !states.has(reference))
1885
+ if (unsupported) throw sourceNodeError(expression, sourceFile, `React useMemo() pure expressions may only reference direct local state; found ${JSON.stringify(unsupported)}`)
1886
+ }
1887
+ const stale = collectionState
1888
+ ? !dependencies.has(collectionState) ? collectionState : undefined
1889
+ : [...states].find(state => referenceIdentifiers(expression, state).length && !dependencies.has(state))
1876
1890
  if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useMemo() must list captured state ${JSON.stringify(stale)} as a dependency`)
1877
1891
  return ts.visitNode(expression, visitor)
1878
1892
  }
@@ -1941,6 +1955,27 @@ function reactMemoExpression(callback) {
1941
1955
  return callback.body.statements[0].expression
1942
1956
  }
1943
1957
 
1958
+ function lowerReactMemoCollectionExpression(expression, factory) {
1959
+ if (!expression) return undefined
1960
+ const visit = node => {
1961
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && node.arguments.length === 1) {
1962
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Array"), "from"), undefined, [visit(node.expression.expression), node.arguments[0]])
1963
+ }
1964
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["filter", "flatMap"].includes(node.expression.name.text)) {
1965
+ return factory.updateCallExpression(node, factory.updatePropertyAccessExpression(node.expression, visit(node.expression.expression), node.expression.name), node.typeArguments, node.arguments)
1966
+ }
1967
+ if (isArrayFromCall(node)) return factory.updateCallExpression(node, node.expression, node.typeArguments, [visit(node.arguments[0]), ...node.arguments.slice(1)])
1968
+ return node
1969
+ }
1970
+ return visit(expression)
1971
+ }
1972
+
1973
+ function reactMemoCollectionState(expression, states, sourceFile) {
1974
+ const setters = new Map([...states].map(state => [state, state]))
1975
+ const fail = (node, message) => { throw sourceNodeError(node, sourceFile, message) }
1976
+ return renderedCollectionSource(expression, setters, undefined, fail, new Set())?.state?.text
1977
+ }
1978
+
1944
1979
  function reactMemoComponentExpression(identifier, sourceFile, factory, context) {
1945
1980
  for (const statement of sourceFile.statements) {
1946
1981
  if (ts.isFunctionDeclaration(statement) && statement.name?.text === identifier.text && statement.body) {
@@ -2898,7 +2933,7 @@ function isArrayFromCall(value) {
2898
2933
  }
2899
2934
 
2900
2935
  function collectionParameters(callback, label, fail) {
2901
- if (!ts.isArrowFunction(callback) || callback.parameters.length < 1 || callback.parameters.length > 2 || callback.parameters.some(parameter => !ts.isIdentifier(parameter.name))) fail(callback, `${label} callback must be an arrow function with (item) or (item, index) identifier parameters`)
2936
+ if (!ts.isArrowFunction(callback) || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || callback.parameters.length < 1 || callback.parameters.length > 2 || callback.parameters.some(parameter => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken)) fail(callback, `${label} callback must be a synchronous arrow function with (item) or (item, index) identifier parameters`)
2902
2937
  return { item: callback.parameters[0].name.text, index: callback.parameters[1]?.name.text }
2903
2938
  }
2904
2939
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",