@kudzujs/core 0.16.27 → 0.16.29

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.27](https://github.com/kudzujs/kudzu/releases/tag/v0.16.27).** Write reusable components, native event handlers, and pure collection expressions; Kudzu specializes supported source into static content and direct browser updates.
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.
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
@@ -138,6 +138,13 @@ interactions, labels, status updates, keyboard focus, and responsive layouts in
138
138
  browser as needed. Binding comments and inert templates mean raw HTML string
139
139
  matches or tag stripping are not reliable checks of rendered text.
140
140
 
141
+ Normal builds also scan generated route HTML after `afterBuild`, reporting counts
142
+ with/without script or modulepreload text markers and unreadable files, with up to
143
+ five paths per category (long paths are truncated). This is a literal text scan,
144
+ not proof of script freedom: comments and inert content can match, inline event
145
+ attributes are not checked, and extra public HTML files are outside the route
146
+ inventory. Quiet and JSON builds omit this summary.
147
+
141
148
  Keep checks bounded and report concise counts and failing paths rather than
142
149
  repeatedly dumping minified artifacts. A missing tool or truncated response is
143
150
  not a pass. After source changes, rerun affected checks. Optional compiler/artifact
@@ -4,11 +4,11 @@ import { serializeStyle } from "./style.js"
4
4
 
5
5
  const imports = new Map()
6
6
  const bindingTargets = new Map()
7
- const conditionTargets = new Map()
7
+ const conditionTargets = globalThis.__KUDZU_CONDITIONS__ && new Map()
8
8
  const mountedBindings = new WeakSet()
9
- const mountedConditions = new WeakSet()
9
+ const mountedConditions = globalThis.__KUDZU_CONDITIONS__ && new WeakSet()
10
10
  const bindingRegistrations = new WeakMap()
11
- const conditionRegistrations = new WeakMap()
11
+ const conditionRegistrations = globalThis.__KUDZU_CONDITIONS__ && new WeakMap()
12
12
  const textDescriptors = globalThis.__KUDZU_TEXT_BINDINGS__ && typeof document !== "undefined" ? JSON.parse(document.body.dataset.kTextBindings ?? "[]") : []
13
13
  const bindingTypes = ["class", "disabled", "value", "checked", "style"]
14
14
  const bindingSelector = [...bindingTypes.map(target => `[data-k-bind-${target}]`), "[data-k-bind-attrs]"].join(",")
@@ -47,6 +47,7 @@ function commitBindings(id) {
47
47
  else patchBinding(binding.node, binding.target, binding.read())
48
48
  }
49
49
  }
50
+ if (!globalThis.__KUDZU_CONDITIONS__) return
50
51
  const conditions = conditionTargets.get(id)
51
52
  if (!conditions) return
52
53
  for (const condition of conditions) {
@@ -57,9 +58,9 @@ function commitBindings(id) {
57
58
 
58
59
  registerCommitter(commitBindings)
59
60
  registerMountHook(mountBindings, "bindings")
60
- registerMountHook(mountConditions, "conditions")
61
+ if (globalThis.__KUDZU_CONDITIONS__) registerMountHook(mountConditions, "conditions")
61
62
  registerUnmountHook(unmountBindings, "bindings")
62
- registerUnmountHook(unmountConditions, "conditions")
63
+ if (globalThis.__KUDZU_CONDITIONS__) registerUnmountHook(unmountConditions, "conditions")
63
64
  registerStateReleaseHook(releaseBindings)
64
65
 
65
66
  if (typeof document !== "undefined") mountDom(document)
@@ -206,6 +207,7 @@ function releaseBindings(id) {
206
207
  bindingRegistrations.delete(binding.node)
207
208
  }
208
209
  bindingTargets.delete(id)
210
+ if (!globalThis.__KUDZU_CONDITIONS__) return
209
211
  for (const condition of conditionTargets.get(id) ?? []) {
210
212
  const registration = conditionRegistrations.get(condition.start)
211
213
  for (const [conditionId, entry] of registration?.registrations ?? []) conditionTargets.get(conditionId)?.delete(entry)
@@ -59,13 +59,14 @@ export async function buildWithSession(project, { changedFiles, quiet = false, m
59
59
  try {
60
60
  await recoverOutput(outputDirectory, backupOutput)
61
61
  await rm(stagedOutput, { recursive: true, force: true })
62
- const { result, pageCount, behaviorCount, cache, inspectionData, explanation } = await buildInto(project, stagedOutput, { changedFiles, minify, quiet, retainCache, explanationRoute })
62
+ const { result, pageCount, behaviorCount, cache, inspectionData, explanation, htmlSummary } = await buildInto(project, stagedOutput, { changedFiles, minify, quiet, retainCache, explanationRoute })
63
63
  const promoteStarted = project.timings ? performance.now() : 0
64
64
  await promoteOutput(stagedOutput, outputDirectory, backupOutput)
65
65
  addTiming(project.timings, "writeMs", promoteStarted)
66
66
  project.buildCache = retainCache ? cache : undefined
67
67
  if (!quiet) {
68
68
  console.log(`Built ${pageCount} page(s), ${behaviorCount} interactive page(s) into dist/`)
69
+ console.log(htmlSummary)
69
70
  console.log("Browser behavior and accessibility need verification: https://kudzujs.cloud/docs#build")
70
71
  }
71
72
  return explanationRoute ? explanation : inspection ? createInspectionReport(inspectionData) : result
@@ -460,7 +461,7 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, quiet
460
461
  const generated = generateEffectRuntime(await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8"), capabilityIR)
461
462
  await writeBundledJavaScript(join(familyDirectory, "kudzu-effect.js"), generated.source, minify, generated.define)
462
463
  }
463
- if (bindings.count || lists.styleCount) await writeJavaScript(join(familyDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
464
+ if (bindings.style || lists.styleCount) await writeJavaScript(join(familyDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
464
465
  if (bindings.count) {
465
466
  const generated = generateBindingRuntime(await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"), capabilityIR, family.navigation)
466
467
  await writeBundledJavaScript(join(familyDirectory, "kudzu-binding.js"), generated.source, minify, generated.define)
@@ -553,6 +554,34 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, quiet
553
554
  if (typeof config.afterBuild !== "function") throw new Error("kudzu.config afterBuild must be a function")
554
555
  await config.afterBuild({ root, outDir: outputDirectory, sourceDir: sourceDirectory, base, routes: plans.map(plan => plan.route), plans, rewrites: sortedRewrites, artifacts })
555
556
  }
557
+ let htmlSummary
558
+ if (!quiet) {
559
+ const counts = { "With markers": 0, Unreadable: 0 }
560
+ const samples = { "With markers": [], Unreadable: [] }
561
+ // Scan final route files, not compiler closure metadata or a previous build's scratch report.
562
+ const outputs = routeRecords.map(record => record.output).sort()
563
+ for (let offset = 0; offset < outputs.length; offset += 64) {
564
+ const batch = outputs.slice(offset, offset + 64)
565
+ const categories = await Promise.all(batch.map(async output => {
566
+ try {
567
+ const html = await readFile(join(outputDirectory, output, "index.html"), "utf8")
568
+ if (/<script(?=[\s/>])|<link(?=[\s/>])[^>]*\bmodulepreload\b/i.test(html)) return "With markers"
569
+ } catch {
570
+ return "Unreadable"
571
+ }
572
+ }))
573
+ for (const [index, category] of categories.entries()) {
574
+ if (!category) continue
575
+ counts[category]++
576
+ if (samples[category].length < 5) {
577
+ const path = join("dist", batch[index], "index.html").replaceAll(sep, "/")
578
+ samples[category].push(JSON.stringify(path.length > 240 ? `${path.slice(0, 240)}...` : path))
579
+ }
580
+ }
581
+ }
582
+ htmlSummary = `Route HTML scan: ${routeRecords.length - counts["With markers"] - counts.Unreadable} without script/modulepreload text markers, ${counts["With markers"]} with markers, ${counts.Unreadable} unreadable.`
583
+ for (const category of Object.keys(counts)) if (counts[category]) htmlSummary += `\n ${category}: ${samples[category].join(", ")}${counts[category] > 5 ? ` (+${counts[category] - 5} more)` : ""}`
584
+ }
556
585
  addTiming(project.timings, "writeMs", writeStarted)
557
586
 
558
587
  let explanation
@@ -570,6 +599,7 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, quiet
570
599
  result: { sourceResults, incremental },
571
600
  inspectionData: { sourceFiles: sourceFiles.map(file => relative(root, file).replaceAll(sep, "/")), sourceResults, compatibility, artifacts },
572
601
  explanation,
602
+ htmlSummary,
573
603
  pageCount: routeRecords.length,
574
604
  behaviorCount,
575
605
  cache: { pageRenders, pageSources, placeholders, sourceResults: sourceResultsByFile }
@@ -13,7 +13,7 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
13
13
  for (const plan of plans) assertRouteIR(plan, { concrete: true })
14
14
  const commandEvents = new Set()
15
15
  const nativeEvents = new Set()
16
- const bindings = { count: 0, text: false, svgConditions: false }
16
+ const bindings = { count: 0, text: false, style: false, conditions: false, svgConditions: false }
17
17
  const lists = {
18
18
  count: 0,
19
19
  styleCount: 0,
@@ -50,6 +50,8 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
50
50
  if (event.native) nativeEvents.add(event.event)
51
51
  }
52
52
  bindings.text ||= plan.bindings.some(binding => binding.target === "text")
53
+ bindings.style ||= plan.bindings.some(binding => binding.target === "style")
54
+ bindings.conditions ||= plan.conditions.length > 0
53
55
  bindings.svgConditions ||= plan.conditions.some(condition => condition.svg)
54
56
  effects.any ||= plan.effects.length > 0
55
57
  effects.derivedDependencies ||= plan.effects.some(effect => effect.dependencyExpressions?.length)
@@ -118,7 +120,7 @@ export function assertCapabilityIR(capabilityIR, records, options = {}) {
118
120
  if (!sections.every(name => isRecord(capabilityIR[name]))) throw new Error("Invalid CapabilityIR v1 structure")
119
121
  if (!["behaviors", "regularBehaviors", "regularStateSeeds", "dependencyStateSeeds"].every(name => isCount(capabilityIR.routes[name]))) throw new Error("Invalid CapabilityIR v1 route counts")
120
122
  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")
121
- if (!isCount(capabilityIR.bindings.count) || !["text", "svgConditions"].every(name => typeof capabilityIR.bindings[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 bindings")
123
+ if (!isCount(capabilityIR.bindings.count) || !["text", "style", "conditions", "svgConditions"].every(name => typeof capabilityIR.bindings[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 bindings")
122
124
  const listFlags = ["conditions", "svg", "deepConditions", "textRanges", "attributes", "events", "expressions", "expressionAttributes", "seeds", "effects", "rowHooks", "rowRefs", "complexRowState", "nested", "selectors", "calculated", "static", "indexes", "stableFastPaths", "generalRowHooks", "asyncParts", "mounts"]
123
125
  if (!isCount(capabilityIR.lists.count) || !isCount(capabilityIR.lists.styleCount) || !listFlags.every(name => typeof capabilityIR.lists[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 lists")
124
126
  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")
@@ -29,12 +29,18 @@ export function generateBindingRuntime(source, capabilityIR, navigable) {
29
29
  assertCapabilityIR(capabilityIR)
30
30
  let runtime = replaceRequired(source, '"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import", "binding-runtime.js")
31
31
  runtime = replaceRequired(runtime, '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "binding-runtime.js")
32
- runtime = replaceRequired(runtime, '"./style.js"', '"./kudzu-style.js"', "style import", "binding-runtime.js")
32
+ runtime = capabilityIR.bindings.style
33
+ ? replaceRequired(runtime, '"./style.js"', '"./kudzu-style.js"', "style import", "binding-runtime.js")
34
+ : replaceSequenceRequired(runtime, [
35
+ ['import { serializeStyle } from "./style.js"\n', "", "unused style import"],
36
+ [' } else if (target === "style") {\n const style = serializeStyle(value)\n if (style) node.setAttribute("style", style)\n else node.removeAttribute("style")\n', "", "unused style branch"]
37
+ ], "binding-runtime.js")
33
38
  if (navigable) runtime = specializeNavigationTextDescriptors(runtime)
34
39
  return {
35
40
  source: runtime,
36
41
  define: {
37
42
  "globalThis.__KUDZU_TEXT_BINDINGS__": String(capabilityIR.bindings.text),
43
+ "globalThis.__KUDZU_CONDITIONS__": String(capabilityIR.bindings.conditions),
38
44
  "globalThis.__KUDZU_SVG_CONDITIONS__": String(capabilityIR.bindings.svgConditions),
39
45
  "globalThis.__KUDZU_CAPTURE_STATE__": String(capabilityIR.captures.nestedState)
40
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.16.27",
3
+ "version": "0.16.29",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",