@kudzujs/core 0.6.18 → 0.6.20

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
@@ -79,7 +79,7 @@ export default function HomePage() {
79
79
  npm run dev
80
80
  ```
81
81
 
82
- Pages live in `src/pages`; `index.tsx` maps to `/`. `npm run dev` serves locally on `127.0.0.1`, reloads the browser after successful rebuilds, and shows build failures in an error overlay. Across that full-page reload, compatible Kudzu logical state is briefly preserved by route-unique state variable name for the current pathname, query, and hash, including controlled properties, conditions, and keyed-list arrays. Renamed, removed, and duplicate-named state is skipped. Uncontrolled DOM state, focus, selection, and imperative DOM mutations are not preserved. Set `PORT` to change the default port of `3000`. The development client and state snapshot are dev-only; production output in `dist/` is unaffected.
82
+ Pages live in `src/pages`; `index.tsx` maps to `/`. `npm run dev` serves locally on `127.0.0.1`, reloads the browser after successful rebuilds, and shows build failures in an error overlay. Across that full-page reload, compatible Kudzu logical state is briefly preserved by route-unique state variable name for the current pathname, query, and hash, including controlled properties, conditions, and keyed-list arrays. Renamed, removed, and duplicate-named state is skipped. Uncontrolled DOM state, focus, selection, and imperative DOM mutations are not preserved. The server starts at `PORT` or `3000` and increments until it finds an available port. Set `HOST=0.0.0.0` when a local reverse proxy or container must reach the server. The development client and state snapshot are dev-only; production output in `dist/` is unaffected.
83
83
 
84
84
  Dynamic static pages use bracket parameters and `getStaticPaths()`:
85
85
 
@@ -65,6 +65,7 @@ export async function build({ quiet = false, minify = true } = {}) {
65
65
  const plans = []
66
66
  const pageEntries = []
67
67
  const effectEntries = []
68
+ const nativeEntries = []
68
69
  const paramEntries = []
69
70
  const rewrites = []
70
71
  const emittedRoutes = new Set()
@@ -103,6 +104,7 @@ export async function build({ quiet = false, minify = true } = {}) {
103
104
  const navigationGroup = navigationByRoute.get(applicationRoute)
104
105
  const navigable = Boolean(navigationGroup)
105
106
  const effectPath = `effects/${route ? `${route}/index` : "index"}.js`
107
+ const nativePath = `native/${route ? `${route}/index` : "index"}.js`
106
108
  const paramPath = `params/${route}/index.js`
107
109
  if (emittedRoutes.has(routePath)) throw new Error(`Duplicate route: ${routePath}`)
108
110
  emittedRoutes.add(routePath)
@@ -126,6 +128,7 @@ export async function build({ quiet = false, minify = true } = {}) {
126
128
  base,
127
129
  runtimeAsset: runtimePlaceholder,
128
130
  effectAsset: assetPath(base, `assets/${effectPath}`),
131
+ nativeAsset: assetPath(base, `assets/${nativePath}`),
129
132
  paramAsset: assetPath(base, `assets/${paramPath}`),
130
133
  runtimeParams: runtimeSchema?.params,
131
134
  ...(navigable ? { navigationAsset: navigationGroup.assetPath, applicationId: navigationGroup.applicationId, layoutId: navigationGroup.layoutId, routeId: applicationRoute } : {})
@@ -141,6 +144,10 @@ export async function build({ quiet = false, minify = true } = {}) {
141
144
  plans.push({ route: routePath, ...result.plan })
142
145
  if (result.hasParams) paramEntries.push({ path: paramPath, schema: runtimeSchema, params: result.plan.params, usesDependencyRuntime, navigable })
143
146
  if (result.hasEffects) effectEntries.push({ path: effectPath, effects: runtimeEffects(result.plan.effects, navigable), paramPath: result.hasParams ? paramPath : undefined, usesDependencyRuntime, navigable })
147
+ if (result.plan.events.some(event => event.native)) nativeEntries.push({
148
+ path: nativePath,
149
+ modules: [...new Set(result.plan.events.filter(event => event.native).map(event => event.native.module))]
150
+ })
144
151
  if (result.hasBehaviors) {
145
152
  behaviorCount++
146
153
  if (!usesDependencyRuntime) regularBehaviorCount++
@@ -196,8 +203,7 @@ export async function build({ quiet = false, minify = true } = {}) {
196
203
  const hasNestedStateCaptures = hasNestedCaptureState(plans)
197
204
  const hasSetterCaptures = hasCaptureType(plans, "setter")
198
205
  const hasEffectCaptures = plans.some(plan => plan.effects.some(effect => Object.keys(effect.scope).length))
199
- const nativeModules = emittedHandlerModules.filter(module => module.hasNativeHandlers).map(module => assetPath(base, `assets/${module.path}`))
200
- const hasNativeHandlers = nativeModules.length > 0
206
+ const hasNativeHandlers = nativeEntries.length > 0
201
207
  const hasEffects = effectEntries.length > 0
202
208
  const hasNavigableEffects = effectEntries.some(entry => entry.navigable)
203
209
  const hasNavigableOwners = effectEntries.some(entry => entry.navigable && entry.effects.some(effect => effect.owner))
@@ -279,9 +285,10 @@ export async function build({ quiet = false, minify = true } = {}) {
279
285
  const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
280
286
  .replace('"./shared-runtime.js"', '"./kudzu.js"')
281
287
  .replace('"./serialization.js"', '"./kudzu-serialization.js"')
282
- await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeNativeRuntime(nativeRuntime, nativeEvents, nativeModules), minify, {
288
+ await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeEvents(nativeRuntime, nativeEvents), minify, {
283
289
  "globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
284
290
  })
291
+ for (const entry of nativeEntries) await printNativeEntry(entry, assetsDirectory, base, minify)
285
292
  }
286
293
  if (navigationGroups.length) {
287
294
  const navigationSource = await readFile(new URL("./navigation-runtime.js", import.meta.url), "utf8")
@@ -411,10 +418,13 @@ function specializeNavigationPatterns(source, enabled) {
411
418
  function fallback`)
412
419
  }
413
420
 
414
- function specializeNativeRuntime(source, events, modules) {
415
- const imports = modules.map((module, index) => `import * as __kNativeModule${index} from ${JSON.stringify(module)}`).join("\n")
416
- const entries = modules.map((module, index) => `[${JSON.stringify(module)}, __kNativeModule${index}]`).join(",")
417
- return `${imports}\n${specializeEvents(source, events).replace(/const modules = new Map\(\[[^\n]*\]\)/, `const modules = new Map([${entries}])`)}`
421
+ async function printNativeEntry(entry, assetsDirectory, base, minify) {
422
+ const output = join(assetsDirectory, entry.path)
423
+ await mkdir(dirname(output), { recursive: true })
424
+ const imports = entry.modules.map((module, index) => `import * as __kNativeModule${index} from ${JSON.stringify(module)}`).join("\n")
425
+ const registrations = entry.modules.map((module, index) => `[${JSON.stringify(module)}, __kNativeModule${index}]`).join(",")
426
+ const runtime = assetPath(base, "assets/kudzu-native.js")
427
+ await writeJavaScript(output, `import { registerNativeModules } from ${JSON.stringify(runtime)}\n${imports}\nregisterNativeModules([${registrations}])`, minify)
418
428
  }
419
429
 
420
430
  function printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName) {
@@ -1360,8 +1370,13 @@ export function parseDevPort(value) {
1360
1370
  return port
1361
1371
  }
1362
1372
 
1363
- export async function dev({ port = parseDevPort(process.env.PORT) } = {}) {
1373
+ export function parseDevHost(value) {
1374
+ return value?.trim() || "127.0.0.1"
1375
+ }
1376
+
1377
+ export async function dev({ port = parseDevPort(process.env.PORT), host = parseDevHost(process.env.HOST) } = {}) {
1364
1378
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid dev server port: ${port}`)
1379
+ if (typeof host !== "string" || !host.trim()) throw new Error(`Invalid dev server host: ${host}`)
1365
1380
  const base = normalizeBase((await loadConfig()).base)
1366
1381
 
1367
1382
  let buildError
@@ -1431,7 +1446,8 @@ export async function dev({ port = parseDevPort(process.env.PORT) } = {}) {
1431
1446
  }
1432
1447
  })
1433
1448
 
1434
- server.listen(port, "127.0.0.1", () => console.log(`Kudzu dev server: http://127.0.0.1:${server.address().port}`))
1449
+ const listeningPort = await listenDevServer(server, port, host)
1450
+ console.log(`Kudzu dev server: http://${host}:${listeningPort}`)
1435
1451
 
1436
1452
  let timer
1437
1453
  let rebuilding = false
@@ -1467,6 +1483,32 @@ export async function dev({ port = parseDevPort(process.env.PORT) } = {}) {
1467
1483
  }
1468
1484
  }
1469
1485
 
1486
+ async function listenDevServer(server, port, host) {
1487
+ let candidate = port
1488
+ while (true) {
1489
+ try {
1490
+ await new Promise((resolve, reject) => {
1491
+ const onError = error => {
1492
+ server.off("listening", onListening)
1493
+ reject(error)
1494
+ }
1495
+ const onListening = () => {
1496
+ server.off("error", onError)
1497
+ resolve()
1498
+ }
1499
+ server.once("error", onError)
1500
+ server.once("listening", onListening)
1501
+ server.listen(candidate, host)
1502
+ })
1503
+ return server.address().port
1504
+ } catch (error) {
1505
+ if (error.code !== "EADDRINUSE" || candidate === 0 || candidate === 65535) throw error
1506
+ console.log(`Port ${candidate} is in use, trying ${candidate + 1}`)
1507
+ candidate++
1508
+ }
1509
+ }
1510
+ }
1511
+
1470
1512
  function injectDevClient(html, session, revision, schema) {
1471
1513
  return `${html}${devClient(session, revision, schema).replace("binding|list|native", "binding|deps|list|native")}`
1472
1514
  }
@@ -55,6 +55,7 @@ export function renderPage<Props = Record<string, never>>(
55
55
  base?: string
56
56
  runtimeAsset?: string
57
57
  effectAsset?: string
58
+ nativeAsset?: string
58
59
  paramAsset?: string
59
60
  runtimeParams?: string[]
60
61
  navigationAsset?: string
@@ -381,7 +381,7 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
381
381
  ? `<script type="module"${capability} src="${escapeAttribute(metadata.runtimeAsset ?? assetPath(metadata.base, "assets/kudzu.js"))}"></script>`
382
382
  : ""
383
383
  const nativeRuntime = renderContext.hasNativeBehaviors
384
- ? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-native.js")}"></script>`
384
+ ? `<script type="module"${capability} src="${escapeAttribute(metadata.nativeAsset ?? assetPath(metadata.base, "assets/kudzu-native.js"))}"></script>`
385
385
  : ""
386
386
  const paramRuntime = renderContext.hasParams
387
387
  ? `<script type="module"${capability} src="${escapeAttribute(metadata.paramAsset)}"></script>`
@@ -675,7 +675,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
675
675
  continue
676
676
  }
677
677
 
678
- const name = rawName === "className" ? "class" : rawName === "htmlFor" ? "for" : svg ? svgAttributeAliases[rawName] ?? rawName : rawName
678
+ const name = rawName === "className" ? "class" : rawName === "htmlFor" ? "for" : rawName === "defaultValue" && tag === "input" ? "value" : svg ? svgAttributeAliases[rawName] ?? rawName : rawName
679
679
  const propertyTarget = name === "class" || name === "disabled" || name === "value" || name === "checked" || name === "style"
680
680
  if (value?.[listFieldMarker]) {
681
681
  attributes += renderAttribute(name, value.value)
@@ -6,6 +6,7 @@ const mountedLists = new WeakSet()
6
6
  const imports = __KUDZU_LIST_ASYNC_PARTS__ ? new Map() : undefined
7
7
  const revisions = __KUDZU_LIST_ASYNC_PARTS__ ? new WeakMap() : undefined
8
8
  const itemParts = new WeakMap()
9
+ const listItems = new WeakMap()
9
10
  const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
10
11
  const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}${__KUDZU_LIST_EFFECTS__ ? ",[data-k-effects]" : ""}`
11
12
 
@@ -25,6 +26,9 @@ registerUnmountHook(unmountLists)
25
26
  if (typeof document !== "undefined") mountDom(document)
26
27
 
27
28
  function mountLists(root) {
29
+ let owner = root.nodeType === Node.ELEMENT_NODE ? root : root.parentElement
30
+ while (owner && !listItems.has(owner)) owner = owner.parentElement
31
+ if (owner) fillListParts(root, listItemParts(root), listItems.get(owner), 0)
28
32
  for (const start of matching(root, "template[data-k-list]")) {
29
33
  if (mountedLists.has(start)) continue
30
34
  mountedLists.add(start)
@@ -136,6 +140,7 @@ function updateList(list) {
136
140
  fillListItem(node, item)
137
141
  if (__KUDZU_LIST_ITEM_HOOKS__) notifyListItem(list.descriptor.state, node)
138
142
  }
143
+ listItems.set(node, item)
139
144
  next.push([token, node])
140
145
  values.set(token, value)
141
146
  }
@@ -234,6 +239,7 @@ function addListRoot(list, { item, key, token, value }) {
234
239
  if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
235
240
  mapListItemParts(list.parts, node)
236
241
  fillListItem(node, item)
242
+ listItems.set(node, item)
237
243
  const parent = list.container ?? list.start.parentNode
238
244
  parent.insertBefore(node, list.boundary)
239
245
  if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) mountDom(node)
@@ -2,6 +2,11 @@ import { browserState, commitDom, registerMountHook, registerUnmountHook } from
2
2
  import { deserialize } from "./serialization.js"
3
3
 
4
4
  const registrations = new WeakMap()
5
+ const modules = new Map()
6
+
7
+ export function registerNativeModules(entries) {
8
+ for (const [url, module] of entries) modules.set(url, module)
9
+ }
5
10
 
6
11
  export function createNativeContext(state, stateIds, commit, serializedScope = {}) {
7
12
  const changed = new Set()
@@ -52,7 +57,6 @@ export function createNativeContext(state, stateIds, commit, serializedScope = {
52
57
 
53
58
  if (typeof document !== "undefined") {
54
59
  const eventNames = ["click", "input", "change", "submit", "keydown", "keyup"]
55
- const modules = new Map([])
56
60
  const mount = root => mountNative(root, eventNames, modules)
57
61
  registerMountHook(mount)
58
62
  registerUnmountHook(unmountNative)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.6.18",
3
+ "version": "0.6.20",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",