@kudzujs/core 0.8.13 → 0.8.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 CHANGED
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
14
14
 
15
15
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Latest release: 0.8.13 - Browser capability migration.** Memos scroll spies, Excalidraw progressive sharing, and Cal.com responsive media queries compile through owned browser effects without React or general capability runtimes. Read the [release notes](./RELEASES.md#0813---browser-capability-migration) or open the [release page](https://kudzujs.cloud/releases/0.8.13).
17
+ **Latest release: 0.8.14 - Localized blog migration.** Browser locale entry, prefixed native links, static MDX with interactive copy/tabs, and continuous canvas animation compile without React, eval, a package router, or a canvas runtime. Read the [release notes](./RELEASES.md#0814---localized-blog-migration) or open the [release page](https://kudzujs.cloud/releases/0.8.14).
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
package/RELEASES.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.14 - Localized blog migration
4
+
5
+ Kudzu 0.8.14 proves a localized MDX blog and continuous imperative canvas can migrate without React, request-time i18n, eval, or a canvas runtime.
6
+
7
+ ### New in 0.8.14
8
+
9
+ - Build-known MDX emits complete static article HTML without `eval()` or `new Function()`.
10
+ - Locale-prefixed `/ko` and `/en` routes build through `getStaticPaths()`, and relative Link replacements automatically prefix native hrefs from the build-known locale.
11
+ - The root route selects a stored locale or reads `navigator.languages`, then preserves query and hash through native `location.replace()`.
12
+ - MDX copy blocks and tabs compile through existing clipboard, state, binding, and conditional capabilities.
13
+ - One `null`-initialized canvas ref may feed an inline effect whose local state survives recursive animation frames, visibility changes, and native input listeners.
14
+ - Bare `IntersectionObserver` and `performance` remain browser globals in route-specific effect ESM.
15
+ - Static sibling routes remain complete zero-JavaScript HTML.
16
+
17
+ ### Fixed in 0.8.14
18
+
19
+ - Effect-owned `IntersectionObserver` instances must disconnect in cleanup.
20
+ - Local animation-frame handles assigned by effects must be cancelled in cleanup.
21
+ - The complete suite passes 151/151 tests.
22
+
23
+ ### Boundary
24
+
25
+ This release does not execute `next-intl`, add request-time `Accept-Language` negotiation, general mutable refs, callbacks shared across effects or handlers, a canvas runtime, or arbitrary MDX component execution. Request-time locale redirects remain host/edge configuration; resource-private drawing state and listeners stay inside one owning effect.
26
+
27
+ ### Upgrade
28
+
29
+ ```bash
30
+ npm install @kudzujs/core@^0.8.14
31
+ ```
32
+
3
33
  ## 0.8.13 - Browser capability migration
4
34
 
5
35
  Kudzu 0.8.13 proves browser-owned migration paths from Memos, Excalidraw, and Cal.com without adding React, general external-store compatibility, or browser capability runtimes.
@@ -8,6 +8,10 @@ Compilation begins from page entries and follows relative runtime imports, re-ex
8
8
 
9
9
  Native platform work remains ordinary source. A direct async handler or directly returned relative custom-hook callback may call `navigator.clipboard.writeText()` and update application-owned success/failure state; Kudzu emits only its existing route handler ESM. Debounced synchronization uses a dependency effect that creates `setTimeout()` work and directly returns `clearTimeout()` cleanup, reusing dependency, conditional, keyed, and route ownership. One directly returned relative custom-hook callback may own one `null`-initialized private timeout ref when it directly clears the previous value, assigns a numeric-literal-delay `setTimeout()`, and an empty-dependency effect directly clears the timer on cleanup. Kudzu lowers that ref to compiler-owned state shared by existing handler and effect contexts. An ordinary effect may instead exclusively own `useRef(0)` as one animation-frame handle when a local callback resets it, one scheduler assigns `requestAnimationFrame()`, and cleanup cancels the pending frame; Kudzu lowers it to the existing effect scope without an animation runtime. Multiple timers, dynamic delays, intervals, ref aliases, cross-effect frame handles, unowned delayed writes, and arbitrary timed callback graphs remain unsupported.
10
10
 
11
+ Imperative canvas migrations use the same effect ownership rather than a component or canvas runtime. One `null`-initialized canvas DOM ref may feed an inline effect whose local variables persist across a recursive animation-frame callback, an `IntersectionObserver`, and native canvas/window listeners; the returned cleanup must cancel the latest frame, disconnect the observer, and remove every listener. Bare `IntersectionObserver` and `performance` identifiers remain browser globals in emitted effect ESM. Component-level mutable value refs and callbacks shared across effects or JSX handlers remain unsupported; move resource-private state and listeners into the owning effect.
12
+
13
+ Localized static migrations do not need a package router. `getStaticPaths()` emits each build-known locale, a small relative Link replacement prefixes native hrefs from the locale prop, and an optional `/` mount effect may choose a stored locale or inspect `navigator.languages` before calling `location.replace()` with the original query and hash. This preserves automatic browser-language entry and locale-prefixed navigation; request-time `Accept-Language` negotiation still requires host or edge configuration. Build-known MDX becomes static `dangerouslySetInnerHTML`, while copy controls, tabs, and other supported interactive components remain ordinary state and handler capabilities beside that HTML.
14
+
11
15
  A top-level component local shaped exactly as `const supported = "literal" in navigator` may control one direct `supported && <Branch />` JSX site. Kudzu emits the branch as false static fallback, evaluates the browser capability once through an existing mount effect, and uses existing state-conditional ownership to mount its DOM and handlers only when supported. The value cannot escape or feed aliases, composed expressions, ternaries, dynamic properties, `canShare()`, or other browser globals.
12
16
 
13
17
  A direct top-level `useSyncExternalStore()` may represent one static `window.matchMedia()` boolean when its three inline callbacks add/remove the same `change` listener, read the same literal query, and return `false` for the server snapshot. Kudzu emits false static fallback state and lowers the subscription to an existing owned effect. This is not general external-store support: imported/parameterized media hooks, dynamic queries, legacy listeners, and arbitrary snapshots remain diagnosed.
@@ -4231,6 +4231,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
4231
4231
  const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
4232
4232
  if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
4233
4233
  if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
4234
+ validateEffectOwnedBrowserResources(callback, returns, effectFail)
4234
4235
  const callbackSource = specializedEffect?.sourceFile ?? sourceFile
4235
4236
  const callbackFile = callbackSource.fileName
4236
4237
  const workerStart = workerReferences.length
@@ -5727,7 +5728,7 @@ function compileOptimizedEvent(expression, setters, factory) {
5727
5728
  }
5728
5729
 
5729
5730
  const nativeGlobals = new Set([
5730
- "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "cancelAnimationFrame", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
5731
+ "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "IntersectionObserver", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "cancelAnimationFrame", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "performance", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
5731
5732
  ])
5732
5733
 
5733
5734
  function rewriteEffectWorkers(callback, file, sourceFile, sourceFiles, workerReferences, factory, context) {
@@ -6130,6 +6131,30 @@ function effectReturns(callback) {
6130
6131
  return { cleanup, cleanups, invalid }
6131
6132
  }
6132
6133
 
6134
+ function validateEffectOwnedBrowserResources(callback, returns, fail) {
6135
+ const observers = []
6136
+ const frameAssignments = []
6137
+ const cancellations = new Set()
6138
+ const disconnected = new Set()
6139
+ const insideCleanup = node => returns.cleanups.some(cleanup => {
6140
+ for (let current = node; current; current = current.parent) if (current === cleanup) return true
6141
+ return false
6142
+ })
6143
+ const visit = node => {
6144
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(unwrapExpression(node.initializer)) && ts.isIdentifier(unwrapExpression(node.initializer).expression) && unwrapExpression(node.initializer).expression.text === "IntersectionObserver") observers.push(node)
6145
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(unwrapExpression(node.left)) && ts.isCallExpression(unwrapExpression(node.right)) && ts.isIdentifier(unwrapExpression(node.right).expression) && unwrapExpression(node.right).expression.text === "requestAnimationFrame") frameAssignments.push(node)
6146
+ if (insideCleanup(node) && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "cancelAnimationFrame" && node.arguments.length === 1 && ts.isIdentifier(unwrapExpression(node.arguments[0]))) cancellations.add(unwrapExpression(node.arguments[0]).text)
6147
+ if (insideCleanup(node) && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === "disconnect" && node.arguments.length === 0) disconnected.add(node.expression.expression.text)
6148
+ ts.forEachChild(node, visit)
6149
+ }
6150
+ visit(callback.body)
6151
+ for (const observer of observers) if (!disconnected.has(observer.name.text)) fail(observer, `IntersectionObserver effects must disconnect ${JSON.stringify(observer.name.text)} in cleanup`)
6152
+ for (const assignment of frameAssignments) {
6153
+ const name = unwrapExpression(assignment.left).text
6154
+ if (!cancellations.has(name)) fail(assignment, `Animation loop effects must cancel ${JSON.stringify(name)} in cleanup`)
6155
+ }
6156
+ }
6157
+
6133
6158
  function printClientImports(entries, handlerPath) {
6134
6159
  const unique = new Map(entries.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry]))
6135
6160
  const groups = Map.groupBy(unique.values(), entry => entry.target)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.13",
3
+ "version": "0.8.14",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",