@kudzujs/core 0.6.28 → 0.7.0

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,7 +8,9 @@ HTML-first TSX framework with synchronous state semantics and no virtual DOM.
8
8
 
9
9
  Kudzu is designed so ordinary common React-shaped TSX can migrate with minimal source restructuring. It keeps familiar function components, props, children, collection rendering, conditions, event handlers, `useState`, reduced `useReducer`, refs, and effects, preferring compiler specialization over imperative DOM rewrites. This is a general migration model, not compatibility for one application. Static components compile to HTML; interactions compile to direct DOM capabilities and external ESM only where used.
10
10
 
11
- > Experimental `0.6.x`: the compiler API and supported TSX surface may change.
11
+ > Experimental `0.7.x`: the compiler API and supported TSX surface may change.
12
+
13
+ **0.7.0:** React-source migration preview. Supported conventional `react` imports now compile to static HTML and capability-specific ESM without React, a VDOM, or hydration. See [release notes](./RELEASES.md#070---react-source-migration-preview).
12
14
 
13
15
  Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
14
16
 
@@ -58,6 +60,22 @@ Configure TypeScript:
58
60
 
59
61
  Make sure application TSX files are included by this `tsconfig.json`. Files outside its `include` may fall into an editor-inferred React project and incorrectly report a missing `react/jsx-runtime` or React event-type errors.
60
62
 
63
+ Existing React migration source may retain conventional imports while components are moved under `src`:
64
+
65
+ ```tsx
66
+ import React, { useState } from "react"
67
+
68
+ export default function Header() {
69
+ const [open, setOpen] = useState(false)
70
+ return <React.Fragment>
71
+ <button onClick={() => setOpen(!open)}>{open ? "Close" : "Menu"}</button>
72
+ {open && <nav>Navigation</nav>}
73
+ </React.Fragment>
74
+ }
75
+ ```
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 `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports must retain those names. Default and namespace React imports currently support `Fragment`; named `Fragment` also works. Aliased hooks, `React.useState`, `memo`, `useMemo`, `useCallback`, React classes, and side-effect or dynamic React imports remain unsupported. A static route using these import forms still emits zero JavaScript.
78
+
61
79
  Create `src/pages/index.tsx`:
62
80
 
63
81
  ```tsx
package/RELEASES.md ADDED
@@ -0,0 +1,44 @@
1
+ # Kudzu Releases
2
+
3
+ ## 0.7.0 - React-source migration preview
4
+
5
+ Kudzu 0.7.0 begins the migration track for ordinary React-shaped landing pages. Existing source may retain conventional supported imports from `react`; Kudzu rewrites those imports to compile-time APIs, pre-renders complete HTML, and emits only the route capabilities that are actually used. React, a virtual DOM, hydration, and a browser component tree are never emitted or executed.
6
+
7
+ ```tsx
8
+ import React, { useState } from "react"
9
+
10
+ export default function Header() {
11
+ const [open, setOpen] = useState(false)
12
+
13
+ return <React.Fragment>
14
+ <button onClick={() => setOpen(!open)}>{open ? "Close" : "Menu"}</button>
15
+ {open && <nav>Navigation</nav>}
16
+ </React.Fragment>
17
+ }
18
+ ```
19
+
20
+ ### New in 0.7.0
21
+
22
+ - Conventional unaliased named imports of supported hooks from `react` compile through Kudzu without loading React.
23
+ - Default, namespace, and named `Fragment` imports are accepted for migration source.
24
+ - Relative function components, props, children, conditions, attributes, text, and event handlers keep their familiar TSX shape.
25
+ - Static routes using the accepted React import forms still ship zero JavaScript.
26
+ - Interactive routes ship direct DOM capabilities only; the landing-page acceptance fixture adds state, text, attribute, condition, and menu-handler capabilities.
27
+ - Emitted modules are checked for surviving runtime React references, and side-effect React imports fail with a source location.
28
+ - Keyed collections now support analyzable `filter`, direct-property `flatMap`, `Array.from`, positional keys, recursively deep sibling child maps, nested conditions, latest-item handlers, multiple serializable row states, effects, and `null` object refs.
29
+
30
+ ### Current boundary
31
+
32
+ This is source migration support, not a React compatibility runtime. Aliased hooks, member hook calls such as `React.useState`, `memo`, `useMemo`, `useCallback`, React classes, React Router, Next-specific components, React UI packages, side-effect imports, and dynamic React imports remain unsupported. Migrate a real route, reduce the first unsupported pattern to a fixture, and extend the compiler one proven blocker at a time.
33
+
34
+ ### Measured fixture
35
+
36
+ The two-route landing fixture retains React imports across relative components. Its static route has no script, while its interactive mobile-menu route emits 10,245 B raw / 5,030 B aggregate gzip JavaScript across seven capability files. Seven clean builds after one warm-up measured a 310.0 ms median on the development machine described in `MIGRATION_ROADMAP.md`.
37
+
38
+ ### Upgrade
39
+
40
+ ```bash
41
+ npm install @kudzujs/core@^0.7.0
42
+ ```
43
+
44
+ New Kudzu source should continue importing APIs from `@kudzujs/core`. Retaining `react` imports is intended for migration input where minimizing source edits matters.
@@ -2,6 +2,8 @@
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 unaliased supported named hooks and default, namespace, or named `Fragment`. `build.mjs` rewrites those module references to `@kudzujs/core` before build-time evaluation. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports. Member hook calls and aliased hooks are deliberately not inferred yet.
6
+
5
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.
6
8
  - `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
7
9
  - `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
@@ -1694,6 +1694,7 @@ async function compile(file, sourceFiles, sourceIndex, base, workerReferences) {
1694
1694
  if (errors.length) {
1695
1695
  throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
1696
1696
  }
1697
+ if (hasReactModuleReference(result.outputText, file)) throw new Error(`${relative(root, file)} Runtime React module references are not supported`)
1697
1698
 
1698
1699
  const output = compiledPath(file)
1699
1700
  await mkdir(resolve(output, ".."), { recursive: true })
@@ -1716,6 +1717,18 @@ async function compile(file, sourceFiles, sourceIndex, base, workerReferences) {
1716
1717
  return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, hasEffects: effectHandlers.length > 0, clientImports: [...clientImports] }
1717
1718
  }
1718
1719
 
1720
+ function hasReactModuleReference(source, file) {
1721
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS)
1722
+ let found = false
1723
+ const visit = node => {
1724
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") found = true
1725
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && node.arguments[0].text === "react") found = true
1726
+ if (!found) ts.forEachChild(node, visit)
1727
+ }
1728
+ visit(sourceFile)
1729
+ return found
1730
+ }
1731
+
1719
1732
  function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, clientImports, workerReferences) {
1720
1733
  return context => sourceFile => {
1721
1734
  const factory = context.factory
@@ -1724,7 +1737,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1724
1737
  ts.setParentRecursive(sourceFile, false)
1725
1738
  rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles)
1726
1739
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
1727
- const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && statement.moduleSpecifier.text === "@kudzujs/core" && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
1740
+ const hasUseEffectImport = 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 === "useEffect"))
1728
1741
  const importedSources = new Map()
1729
1742
  const importedSource = target => {
1730
1743
  let imported = importedSources.get(target)
@@ -2208,6 +2221,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2208
2221
  fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
2209
2222
  }
2210
2223
 
2224
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
2225
+ if (!node.importClause) fail(node, "Side-effect React imports are not supported because Kudzu does not load the React runtime")
2226
+ if (node.importClause.isTypeOnly) return node
2227
+ return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral("@kudzujs/core"), node.attributes)
2228
+ }
2229
+
2211
2230
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
2212
2231
  const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
2213
2232
  return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
@@ -3,6 +3,7 @@ export type Reducer<State, Action> = (state: State, action: Action) => State
3
3
  export type Dispatch<Action> = (action: Action) => void
4
4
  export type EffectCleanup = () => void | Promise<void>
5
5
  export type EffectDependency = string | number | boolean | null
6
+ export const Fragment: unique symbol
6
7
 
7
8
  export function useState<T>(initialValue: T): [T, StateSetter<T>]
8
9
  export function useReducer<State, Action>(reducer: Reducer<State, Action>, initialValue: State): [State, Dispatch<Action>]
@@ -22,6 +23,9 @@ export interface Context<T> {
22
23
  export function createContext<T>(defaultValue: T): Context<T>
23
24
  export function useContext<T>(context: Context<T>): T
24
25
 
26
+ declare const React: { Fragment: typeof Fragment }
27
+ export default React
28
+
25
29
  export function behavior(commands: Array<["add" | "set" | "log", unknown, unknown]>): unknown
26
30
  export function nativeBehavior(module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
27
31
  export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
@@ -20,6 +20,7 @@ const contextMarker = Symbol("kudzu.context")
20
20
  const contextProviderMarker = Symbol("kudzu.contextProvider")
21
21
  const routeScopeMarker = Symbol("kudzu.routeScope")
22
22
  const noSelectValue = Symbol("kudzu.no-select-value")
23
+ export const Fragment = Symbol.for("kudzu.fragment")
23
24
  const svgAttributeAliases = {
24
25
  clipRule: "clip-rule",
25
26
  colorInterpolation: "color-interpolation",
@@ -170,6 +171,8 @@ export function useContext(context) {
170
171
  return context.defaultValue
171
172
  }
172
173
 
174
+ export default { Fragment }
175
+
173
176
  export function behavior(commands) {
174
177
  return {
175
178
  [behaviorMarker]: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.6.28",
3
+ "version": "0.7.0",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,6 +26,7 @@
26
26
  "framework/",
27
27
  "GOAL_A.md",
28
28
  "GOAL_B.md",
29
+ "RELEASES.md",
29
30
  "README.md",
30
31
  "LICENSE"
31
32
  ],