@kudzujs/core 0.16.29 → 0.16.32

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.16.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Current release: [0.16.29](https://github.com/kudzujs/kudzu/releases/tag/v0.16.29).** Write reusable components, native event handlers, and pure collection expressions; Kudzu specializes supported source into static content and direct browser updates, excluding unused style and conditional capabilities.
17
+ **Current release: [0.16.32](https://github.com/kudzujs/kudzu/releases/tag/v0.16.32).** Write reusable components, native event handlers, and pure collection expressions; Kudzu specializes supported source into static content and direct browser updates. Runtime families whose branches own no state omit conditional state lifecycle code, removing 334 raw JavaScript bytes from the search fixture while preserving stateful remounts. AI-cost and 1.0 gates remain blocked.
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
@@ -16,25 +16,25 @@ const bindingSelector = [...bindingTypes.map(target => `[data-k-bind-${target}]`
16
16
  export function patchBinding(node, target, value) {
17
17
  if (globalThis.__KUDZU_TEXT_BINDINGS__ && target === "text") {
18
18
  patchText(node, value)
19
- } else if (target === "disabled") {
19
+ } else if (globalThis.__KUDZU_DISABLED_BINDINGS__ !== false && target === "disabled") {
20
20
  node.toggleAttribute("disabled", Boolean(value))
21
- } else if (target === "checked") {
21
+ } else if (globalThis.__KUDZU_CHECKED_BINDINGS__ !== false && target === "checked") {
22
22
  node.checked = Boolean(value)
23
- } else if (target === "value") {
23
+ } else if (globalThis.__KUDZU_VALUE_BINDINGS__ !== false && target === "value") {
24
24
  const next = value == null ? "" : String(value)
25
25
  if (node.value !== next) node.value = next
26
26
  } else if (target === "style") {
27
27
  const style = serializeStyle(value)
28
28
  if (style) node.setAttribute("style", style)
29
29
  else node.removeAttribute("style")
30
- } else if (target === "class" && (value == null || value === false)) {
30
+ } else if (globalThis.__KUDZU_CLASS_BINDINGS__ !== false && target === "class" && (value == null || value === false)) {
31
31
  node.removeAttribute("class")
32
- } else if (target === "class") {
32
+ } else if (globalThis.__KUDZU_CLASS_BINDINGS__ !== false && target === "class") {
33
33
  if (node.namespaceURI === "http://www.w3.org/1999/xhtml") node.className = String(value)
34
34
  else node.setAttribute("class", String(value))
35
- } else if (value == null || (value === false && !isStringBooleanAttribute(target))) {
35
+ } else if (globalThis.__KUDZU_ATTRIBUTE_BINDINGS__ !== false && (value == null || (value === false && !isStringBooleanAttribute(target)))) {
36
36
  node.removeAttribute(target)
37
- } else {
37
+ } else if (globalThis.__KUDZU_ATTRIBUTE_BINDINGS__ !== false) {
38
38
  node.setAttribute(target, value === true && !isStringBooleanAttribute(target) ? "" : String(value))
39
39
  }
40
40
  }
@@ -66,15 +66,15 @@ registerStateReleaseHook(releaseBindings)
66
66
  if (typeof document !== "undefined") mountDom(document)
67
67
 
68
68
  function mountBindings(root) {
69
- for (const node of matching(root, bindingSelector)) {
69
+ if (globalThis.__KUDZU_ELEMENT_BINDINGS__ !== false) for (const node of matching(root, bindingSelector)) {
70
70
  if (mountedBindings.has(node)) continue
71
71
  mountedBindings.add(node)
72
72
  const registrations = []
73
73
  bindingRegistrations.set(node, registrations)
74
- const descriptors = bindingTypes.flatMap(target => node.hasAttribute(`data-k-bind-${target}`)
74
+ const descriptors = globalThis.__KUDZU_PROPERTY_BINDINGS__ !== false ? bindingTypes.flatMap(target => node.hasAttribute(`data-k-bind-${target}`)
75
75
  ? [[target, JSON.parse(node.dataset[`kBind${capitalize(target)}`])]]
76
- : [])
77
- if (node.dataset.kBindAttrs) descriptors.push(...JSON.parse(node.dataset.kBindAttrs).map(({ target, ...descriptor }) => [target, descriptor]))
76
+ : []) : []
77
+ if (globalThis.__KUDZU_ATTRIBUTE_BINDINGS__ !== false && node.dataset.kBindAttrs) descriptors.push(...JSON.parse(node.dataset.kBindAttrs).map(({ target, ...descriptor }) => [target, descriptor]))
78
78
  for (const [target, descriptor] of descriptors) {
79
79
  if (descriptor.state) {
80
80
  const binding = { node, target, read: Object.hasOwn(descriptor, "truthy") ? () => browserState.get(descriptor.state) ? descriptor.truthy : descriptor.falsy : () => browserState.get(descriptor.state) }
@@ -131,8 +131,8 @@ function mountConditions(root) {
131
131
  const truthy = globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg ? start.dataset.kSvgTrue : start.content.querySelector("template[data-k-true]")
132
132
  const falsy = globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg ? start.dataset.kSvgFalse : start.content.querySelector("template[data-k-false]")
133
133
  if (!end || !truthy || !falsy) continue
134
- const condition = { start, end, truthy, falsy, svg: globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg, kind: descriptor.kind, current: conditionKey(descriptor.kind, descriptor.initial), mount: descriptor.mount, owned: descriptor.owned }
135
- mountConditionStates(condition, Boolean(descriptor.initial), false)
134
+ const condition = { start, end, truthy, falsy, svg: globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg, kind: descriptor.kind, current: conditionKey(descriptor.kind, descriptor.initial), mount: descriptor.mount, owned: globalThis.__KUDZU_CONDITION_STATE__ !== false && descriptor.owned }
135
+ if (globalThis.__KUDZU_CONDITION_STATE__ !== false) mountConditionStates(condition, Boolean(descriptor.initial), false)
136
136
  const mount = evaluator => {
137
137
  if (!start.isConnected) return
138
138
  condition.read = evaluator.read
@@ -155,7 +155,7 @@ function updateCondition(condition) {
155
155
  if (next === condition.current) return
156
156
  const previous = condition.current === "true"
157
157
  removeConditionRange(condition.start, condition.end, condition.mount)
158
- unmountConditionStates(condition, previous)
158
+ if (globalThis.__KUDZU_CONDITION_STATE__ !== false) unmountConditionStates(condition, previous)
159
159
  const truthy = Boolean(value)
160
160
  const falseText = condition.kind === "and" && !truthy ? renderFalsy(value) : ""
161
161
  const fragment = falseText
@@ -164,11 +164,11 @@ function updateCondition(condition) {
164
164
  ? svgFragment(condition.start, truthy ? condition.truthy : condition.falsy)
165
165
  : (truthy ? condition.truthy : condition.falsy).content.cloneNode(true)
166
166
  const nodes = condition.mount ? [...fragment.childNodes] : undefined
167
- mountConditionStates(condition, truthy, true)
167
+ if (globalThis.__KUDZU_CONDITION_STATE__ !== false) mountConditionStates(condition, truthy, true)
168
168
  condition.end.parentNode.insertBefore(fragment, condition.end)
169
169
  condition.current = next
170
170
  if (condition.mount) for (const node of nodes) mountDom(node)
171
- const select = condition.start.closest("select[data-k-bind-value]")
171
+ const select = globalThis.__KUDZU_VALUE_BINDINGS__ !== false && condition.start.closest("select[data-k-bind-value]")
172
172
  if (select) {
173
173
  for (const binding of new Set((bindingRegistrations.get(select) ?? []).map(([, entry]) => entry))) {
174
174
  patchBinding(binding.node, binding.target, binding.read())
@@ -177,7 +177,7 @@ function updateCondition(condition) {
177
177
  }
178
178
 
179
179
  function unmountBindings(root) {
180
- for (const node of matching(root, bindingSelector)) {
180
+ if (globalThis.__KUDZU_ELEMENT_BINDINGS__ !== false) for (const node of matching(root, bindingSelector)) {
181
181
  for (const [id, binding] of bindingRegistrations.get(node) ?? []) bindingTargets.get(id)?.delete(binding)
182
182
  bindingRegistrations.delete(node)
183
183
  mountedBindings.delete(node)
@@ -195,7 +195,7 @@ function unmountConditions(root) {
195
195
  for (const start of matching(root, "template[data-k-if]")) {
196
196
  const registration = conditionRegistrations.get(start)
197
197
  for (const [id, condition] of registration?.registrations ?? []) conditionTargets.get(id)?.delete(condition)
198
- if (registration?.condition) unmountConditionStates(registration.condition, registration.condition.current === "true")
198
+ if (globalThis.__KUDZU_CONDITION_STATE__ !== false && registration?.condition) unmountConditionStates(registration.condition, registration.condition.current === "true")
199
199
  conditionRegistrations.delete(start)
200
200
  mountedConditions.delete(start)
201
201
  }
@@ -211,7 +211,7 @@ function releaseBindings(id) {
211
211
  for (const condition of conditionTargets.get(id) ?? []) {
212
212
  const registration = conditionRegistrations.get(condition.start)
213
213
  for (const [conditionId, entry] of registration?.registrations ?? []) conditionTargets.get(conditionId)?.delete(entry)
214
- unmountConditionStates(condition, condition.current === "true")
214
+ if (globalThis.__KUDZU_CONDITION_STATE__ !== false) unmountConditionStates(condition, condition.current === "true")
215
215
  conditionRegistrations.delete(condition.start)
216
216
  }
217
217
  conditionTargets.delete(id)
@@ -1,6 +1,8 @@
1
1
  import { assertRouteBuildRecord } from "./route-build-record.mjs"
2
2
  import { assertJsonSafe, assertRouteIR } from "./route-ir.mjs"
3
3
 
4
+ const propertyBindings = ["class", "disabled", "value", "checked", "style"]
5
+
4
6
  export function usesRouteDependencyRuntime({ plan, navigable, hasBindings, hasLists }, validate = true) {
5
7
  if (validate) assertRouteIR(plan)
6
8
  const hasDependencies = plan.effects.some(effect => effect.dependencies?.length)
@@ -13,7 +15,7 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
13
15
  for (const plan of plans) assertRouteIR(plan, { concrete: true })
14
16
  const commandEvents = new Set()
15
17
  const nativeEvents = new Set()
16
- const bindings = { count: 0, text: false, style: false, conditions: false, svgConditions: false }
18
+ const bindings = { count: 0, text: false, style: false, conditions: false, conditionState: false, svgConditions: false }
17
19
  const lists = {
18
20
  count: 0,
19
21
  styleCount: 0,
@@ -52,6 +54,7 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
52
54
  bindings.text ||= plan.bindings.some(binding => binding.target === "text")
53
55
  bindings.style ||= plan.bindings.some(binding => binding.target === "style")
54
56
  bindings.conditions ||= plan.conditions.length > 0
57
+ bindings.conditionState ||= plan.conditions.some(condition => Object.values(condition.owned ?? {}).some(states => states.length > 0))
55
58
  bindings.svgConditions ||= plan.conditions.some(condition => condition.svg)
56
59
  effects.any ||= plan.effects.length > 0
57
60
  effects.derivedDependencies ||= plan.effects.some(effect => effect.dependencyExpressions?.length)
@@ -92,6 +95,9 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
92
95
  dependencyStateSeeds: routeEntries.filter(route => route.hasStateSeed && route.usesDependencyRuntime).length
93
96
  }
94
97
  bindings.count = routeEntries.filter(route => route.hasBindings).length
98
+ const bindingTargets = new Set(plans.flatMap(plan => plan.bindings.map(binding => binding.target)))
99
+ bindings.properties = propertyBindings.filter(target => bindingTargets.has(target))
100
+ bindings.attributes = [...bindingTargets].some(target => target !== "text" && !propertyBindings.includes(target))
95
101
  lists.count = routeEntries.filter(route => route.hasLists).length
96
102
  lists.styleCount = routeEntries.filter(route => route.hasListStyles).length
97
103
  lists.generalRowHooks ||= lists.rowRefs || lists.complexRowState
@@ -120,7 +126,10 @@ export function assertCapabilityIR(capabilityIR, records, options = {}) {
120
126
  if (!sections.every(name => isRecord(capabilityIR[name]))) throw new Error("Invalid CapabilityIR v1 structure")
121
127
  if (!["behaviors", "regularBehaviors", "regularStateSeeds", "dependencyStateSeeds"].every(name => isCount(capabilityIR.routes[name]))) throw new Error("Invalid CapabilityIR v1 route counts")
122
128
  if (!["command", "native"].every(name => Array.isArray(capabilityIR.events[name]) && capabilityIR.events[name].every(event => typeof event === "string")) || typeof capabilityIR.events.hasNativeHandlers !== "boolean") throw new Error("Invalid CapabilityIR v1 events")
123
- if (!isCount(capabilityIR.bindings.count) || !["text", "style", "conditions", "svgConditions"].every(name => typeof capabilityIR.bindings[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 bindings")
129
+ if (!isCount(capabilityIR.bindings.count) || !["text", "style", "conditions", "conditionState", "svgConditions", "attributes"].every(name => typeof capabilityIR.bindings[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 bindings")
130
+ if (capabilityIR.bindings.conditionState && !capabilityIR.bindings.conditions) throw new Error("CapabilityIR conditional state requires conditions")
131
+ if (!Array.isArray(capabilityIR.bindings.properties) || capabilityIR.bindings.properties.some(target => !propertyBindings.includes(target)) || new Set(capabilityIR.bindings.properties).size !== capabilityIR.bindings.properties.length) throw new Error("Invalid CapabilityIR v1 binding properties")
132
+ if (capabilityIR.bindings.style !== capabilityIR.bindings.properties.includes("style")) throw new Error("CapabilityIR binding style flag does not match its properties")
124
133
  const listFlags = ["conditions", "svg", "deepConditions", "textRanges", "attributes", "events", "expressions", "expressionAttributes", "seeds", "effects", "rowHooks", "rowRefs", "complexRowState", "nested", "selectors", "calculated", "static", "indexes", "stableFastPaths", "generalRowHooks", "asyncParts", "mounts"]
125
134
  if (!isCount(capabilityIR.lists.count) || !isCount(capabilityIR.lists.styleCount) || !listFlags.every(name => typeof capabilityIR.lists[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 lists")
126
135
  if (!["any", "derivedDependencies", "itemDependencies", "captures", "navigable", "navigableOwners"].every(name => typeof capabilityIR.effects[name] === "boolean") || !["nestedState", "setter"].every(name => typeof capabilityIR.captures[name] === "boolean") || !["shared", "dependency"].every(name => typeof capabilityIR.runtime[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 effect, capture, or runtime flags")
@@ -27,8 +27,13 @@ export function generateEffectRuntime(source, capabilityIR) {
27
27
 
28
28
  export function generateBindingRuntime(source, capabilityIR, navigable) {
29
29
  assertCapabilityIR(capabilityIR)
30
+ const { properties, attributes } = capabilityIR.bindings
30
31
  let runtime = replaceRequired(source, '"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import", "binding-runtime.js")
31
32
  runtime = replaceRequired(runtime, '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "binding-runtime.js")
33
+ runtime = replaceRequired(runtime, /const bindingTypes = \[[^\n]+\]/, `const bindingTypes = ${JSON.stringify(properties)}`, "binding properties", "binding-runtime.js")
34
+ const selectors = [...properties.map(target => `[data-k-bind-${target}]`), ...(attributes ? ["[data-k-bind-attrs]"] : [])]
35
+ // The existing generic selector is smaller when all five property kinds are used.
36
+ if (properties.length !== 5 || !attributes) runtime = replaceRequired(runtime, /^const bindingSelector = .*$/m, `const bindingSelector = ${JSON.stringify(selectors.join(","))}`, "binding selectors", "binding-runtime.js")
32
37
  runtime = capabilityIR.bindings.style
33
38
  ? replaceRequired(runtime, '"./style.js"', '"./kudzu-style.js"', "style import", "binding-runtime.js")
34
39
  : replaceSequenceRequired(runtime, [
@@ -40,7 +45,15 @@ export function generateBindingRuntime(source, capabilityIR, navigable) {
40
45
  source: runtime,
41
46
  define: {
42
47
  "globalThis.__KUDZU_TEXT_BINDINGS__": String(capabilityIR.bindings.text),
48
+ "globalThis.__KUDZU_ELEMENT_BINDINGS__": String(selectors.length > 0),
49
+ "globalThis.__KUDZU_PROPERTY_BINDINGS__": String(properties.length > 0),
50
+ "globalThis.__KUDZU_ATTRIBUTE_BINDINGS__": String(attributes),
51
+ "globalThis.__KUDZU_CLASS_BINDINGS__": String(properties.includes("class")),
52
+ "globalThis.__KUDZU_DISABLED_BINDINGS__": String(properties.includes("disabled")),
53
+ "globalThis.__KUDZU_CHECKED_BINDINGS__": String(properties.includes("checked")),
54
+ "globalThis.__KUDZU_VALUE_BINDINGS__": String(properties.includes("value")),
43
55
  "globalThis.__KUDZU_CONDITIONS__": String(capabilityIR.bindings.conditions),
56
+ "globalThis.__KUDZU_CONDITION_STATE__": String(capabilityIR.bindings.conditionState),
44
57
  "globalThis.__KUDZU_SVG_CONDITIONS__": String(capabilityIR.bindings.svgConditions),
45
58
  "globalThis.__KUDZU_CAPTURE_STATE__": String(capabilityIR.captures.nestedState)
46
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.16.29",
3
+ "version": "0.16.32",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",