@kudzujs/core 0.7.4 → 0.7.5

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.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).
13
+ **0.7.5:** Class composition migration. Direct `clsx` calls compile to ordinary reactive class expressions without shipping the package, and mixed React type imports erase cleanly. See [release notes](./RELEASES.md#075---class-composition-migration).
14
14
 
15
15
  Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
16
16
 
@@ -76,6 +76,8 @@ export default function Header() {
76
76
 
77
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
+ Direct default or named `clsx` imports compile away for string/number literals, literal arrays, literal object conditions, and conditional expressions. Kudzu lowers those calls to ordinary class expressions, so reactive classes reuse existing bindings without shipping `clsx`; spreads, computed object keys, arbitrary calls, and indirect references remain unsupported.
80
+
79
81
  Create `src/pages/index.tsx`:
80
82
 
81
83
  ```tsx
package/RELEASES.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.7.5 - Class composition migration
4
+
5
+ Kudzu 0.7.5 lets ordinary React source retain common direct `clsx` calls while compiling them to existing static and reactive class paths.
6
+
7
+ ### New in 0.7.5
8
+
9
+ - Default and named `clsx` imports lower at build time for string and number literals, literal arrays, literal object conditions, and conditional expressions.
10
+ - Dynamic object conditions reuse existing reactive class bindings without serializing or shipping the `clsx` function.
11
+ - Static uses add no browser JavaScript, and the package import is erased from compiled modules.
12
+ - Mixed React imports such as `import { useState, type ReactNode } from "react"` now erase type-only specifiers before runtime module rewriting.
13
+ - The React/Vite fixture verifies initial class output, state-driven class updates in Chrome, package erasure, and mixed type imports.
14
+
15
+ ### Boundary
16
+
17
+ `clsx` spreads, computed object keys, arbitrary calls, and indirect references remain unsupported. This is source lowering for a proven migration pattern, not package execution or a general React ecosystem runtime.
18
+
19
+ ### Upgrade
20
+
21
+ ```bash
22
+ npm install @kudzujs/core@^0.7.5
23
+ ```
24
+
3
25
  ## 0.7.4 - Memoized collection pipelines
4
26
 
5
27
  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.
@@ -4,6 +4,8 @@ Kudzu specializes ordinary common React-shaped TSX so migrations need minimal so
4
4
 
5
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
+ 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
+
7
9
  - `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
10
  - `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
9
11
  - `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
@@ -1744,6 +1744,53 @@ function hasReactModuleReference(source, file) {
1744
1744
  return found
1745
1745
  }
1746
1746
 
1747
+ function normalizeClsxSyntax(sourceFile, factory, context) {
1748
+ const names = new Set()
1749
+ for (const statement of sourceFile.statements) {
1750
+ if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "clsx") continue
1751
+ if (statement.importClause?.name) names.add(statement.importClause.name.text)
1752
+ const bindings = statement.importClause?.namedBindings
1753
+ if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) if (!entry.isTypeOnly && (entry.propertyName ?? entry.name).text === "clsx") names.add(entry.name.text)
1754
+ }
1755
+ if (!names.size) return sourceFile
1756
+
1757
+ const lower = node => {
1758
+ node = unwrapExpression(node)
1759
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return node
1760
+ if (node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword) return factory.createStringLiteral("")
1761
+ if (ts.isConditionalExpression(node)) return factory.updateConditionalExpression(node, node.condition, node.questionToken, lower(node.whenTrue), node.colonToken, lower(node.whenFalse))
1762
+ if (ts.isArrayLiteralExpression(node)) return combine(node.elements.map(lower))
1763
+ if (ts.isObjectLiteralExpression(node)) return combine(node.properties.map(property => {
1764
+ if (!ts.isPropertyAssignment(property) || property.name && ts.isComputedPropertyName(property.name)) throw sourceNodeError(property, sourceFile, "clsx() object arguments require ordinary key/value properties")
1765
+ const name = property.name
1766
+ const value = name && (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) ? name.text : undefined
1767
+ if (value === undefined) throw sourceNodeError(property, sourceFile, "clsx() object keys must be identifiers or literals")
1768
+ return factory.createConditionalExpression(property.initializer, undefined, factory.createStringLiteral(value), undefined, factory.createStringLiteral(""))
1769
+ }))
1770
+ throw sourceNodeError(node, sourceFile, "clsx() arguments must be string/number literals, literal arrays, literal objects, or conditionals")
1771
+ }
1772
+ const combine = entries => entries.length ? entries.reduce((result, entry) => factory.createBinaryExpression(factory.createBinaryExpression(result, factory.createToken(ts.SyntaxKind.PlusToken), factory.createStringLiteral(" ")), factory.createToken(ts.SyntaxKind.PlusToken), entry)) : factory.createStringLiteral("")
1773
+
1774
+ const visitor = node => {
1775
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && names.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) return combine(node.arguments.map(lower))
1776
+ if (ts.isIdentifier(node) && names.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "clsx imports may only be called directly")
1777
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "clsx") {
1778
+ const clause = node.importClause
1779
+ if (!clause || clause.isTypeOnly) return node
1780
+ let bindings = clause.namedBindings
1781
+ if (bindings && ts.isNamedImports(bindings)) {
1782
+ const elements = bindings.elements.filter(entry => entry.isTypeOnly || (entry.propertyName ?? entry.name).text !== "clsx")
1783
+ bindings = elements.length ? factory.updateNamedImports(bindings, elements) : undefined
1784
+ }
1785
+ const defaultName = clause.name && names.has(clause.name.text) ? undefined : clause.name
1786
+ if (!defaultName && !bindings) return undefined
1787
+ return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, defaultName, bindings), node.moduleSpecifier, node.attributes)
1788
+ }
1789
+ return ts.visitEachChild(node, visitor, context)
1790
+ }
1791
+ return ts.visitNode(sourceFile, visitor)
1792
+ }
1793
+
1747
1794
  function normalizeReactMigrationSyntax(sourceFile, factory, context) {
1748
1795
  const supported = new Set(["createContext", "useContext", "useEffect", "useReducer", "useRef", "useState"])
1749
1796
  const erased = new Set(["memo", "useCallback", "useMemo"])
@@ -1903,6 +1950,7 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
1903
1950
  const entries = []
1904
1951
  for (const entry of bindings.elements) {
1905
1952
  const name = (entry.propertyName ?? entry.name).text
1953
+ if (entry.isTypeOnly) continue
1906
1954
  if (!entry.isTypeOnly && erased.has(name)) continue
1907
1955
  if (!entry.isTypeOnly && supported.has(name)) {
1908
1956
  if (imported.has(name)) continue
@@ -2014,6 +2062,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2014
2062
  return context => sourceFile => {
2015
2063
  const factory = context.factory
2016
2064
  const hasLinkElements = /<link/i.test(sourceFile.text)
2065
+ sourceFile = normalizeClsxSyntax(sourceFile, factory, context)
2066
+ ts.setParentRecursive(sourceFile, false)
2017
2067
  sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context)
2018
2068
  ts.setParentRecursive(sourceFile, false)
2019
2069
  sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
@@ -2025,7 +2075,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2025
2075
  const importedSource = target => {
2026
2076
  let imported = importedSources.get(target)
2027
2077
  if (!imported) {
2028
- imported = normalizeReactMigrationSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context)
2078
+ imported = normalizeClsxSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context)
2079
+ ts.setParentRecursive(imported, false)
2080
+ imported = normalizeReactMigrationSyntax(imported, factory, context)
2029
2081
  ts.setParentRecursive(imported, false)
2030
2082
  imported = normalizeRenderControlFlow(imported, factory, context)
2031
2083
  ts.setParentRecursive(imported, false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",