@kudzujs/core 0.8.53 → 0.8.56

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.
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path"
2
2
 
3
3
  export function createParamCodegen({ browserPath, inlineJson, relativeModulePath }) {
4
- return function printParamEntry(schema, params, searchParams, searchParamsWritable, output, assetsDirectory, base, runtimeName, navigable) {
4
+ return function printParamEntry(schema, params, searchParams, searchParamsWritable, output, runtimeDirectory, base, runtimeName, navigable) {
5
5
  const hasSearch = searchParams.length || searchParamsWritable
6
6
  const signature = hasSearch ? "pathname, search" : "pathname"
7
7
  const prefix = navigable ? `export function initializeParams(${signature}) {\n${searchParamsWritable ? "globalThis.__kSetSearchParams = setSearchParams\n" : ""}` : `${schema ? "let pathname = location.pathname\n" : ""}${hasSearch ? "let search = location.search\n" : ""}`
@@ -66,7 +66,7 @@ function setSearchParams(update, replace) {
66
66
  }
67
67
  ${navigable ? "" : `globalThis.__kSetSearchParams = setSearchParams
68
68
  addEventListener("popstate", () => ${searchParams.length ? "initializeSearch(location.search)" : "undefined"})`}` : ""
69
- return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}
69
+ return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, runtimeName)))}
70
70
  ${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}`
71
71
  }
72
72
  }
@@ -0,0 +1,133 @@
1
+ import { dirname, relative, resolve, sep } from "node:path"
2
+ import { assetPath } from "./path-helpers.mjs"
3
+ import { assertRouteBuildRecord } from "./route-build-record.mjs"
4
+ import { planRouteCapabilities } from "./route-capability-planner.mjs"
5
+ import { capabilitySignature, planRuntimeFamilies } from "./runtime-family-planner.mjs"
6
+
7
+ export function createRouteArtifactReport(records, {
8
+ base = "",
9
+ handlerMetafile,
10
+ outputDirectory,
11
+ navigationAssets = new Map(),
12
+ runtimeFamilies,
13
+ runtimeFamilyByRecord,
14
+ workerReferences = [],
15
+ workerOutputs = new Map()
16
+ } = {}) {
17
+ for (const record of records) assertRouteBuildRecord(record)
18
+ if (!runtimeFamilies || !runtimeFamilyByRecord) ({ families: runtimeFamilies, familyByRecord: runtimeFamilyByRecord } = planRuntimeFamilies(records))
19
+ const handlerGraph = handlerMetafile ? outputGraph(handlerMetafile, outputDirectory, base) : new Map()
20
+ const routes = records.map(record => {
21
+ const capability = planRouteCapabilities([record], { navigationRouteCount: Number(record.capabilities.navigable) })
22
+ const handlerEntries = [...new Set(record.artifacts.handlers.map(reference => reference.module))].sort()
23
+ const handlerOutputs = closure(handlerEntries, handlerGraph, Boolean(handlerMetafile))
24
+ const workers = workerReferences
25
+ .filter(reference => record.artifacts.effects.some(effect => effect.module === reference.module && effect.handler === reference.handler))
26
+ .map(reference => {
27
+ const output = workerOutputs.get(reference.placeholder)
28
+ if (!output) throw new Error(`Worker output was not recorded: ${reference.root}`)
29
+ return { source: reference.root, entry: output.entry, chunks: [...output.chunks].sort() }
30
+ })
31
+ .sort((left, right) => left.source.localeCompare(right.source) || left.entry.localeCompare(right.entry))
32
+ return {
33
+ route: record.route,
34
+ capability: {
35
+ signature: capabilitySignature(capability),
36
+ manifest: capability
37
+ },
38
+ runtime: routeRuntimeEdges(record, capability, runtimeFamilyByRecord.get(record), base, navigationAssets.get(record.route)),
39
+ handlers: {
40
+ entries: handlerEntries,
41
+ chunks: handlerOutputs.filter(output => !handlerEntries.includes(output))
42
+ },
43
+ workers,
44
+ styles: [...record.artifacts.styles].sort()
45
+ }
46
+ }).sort((left, right) => left.route.localeCompare(right.route))
47
+ const owners = new Map()
48
+ for (const route of routes) {
49
+ for (const path of [...route.handlers.chunks, ...route.workers.flatMap(worker => worker.chunks)]) {
50
+ const paths = owners.get(path) ?? new Set()
51
+ paths.add(route.route)
52
+ owners.set(path, paths)
53
+ }
54
+ }
55
+ return {
56
+ version: 2,
57
+ runtimeFamilies: runtimeFamilies.map(family => ({
58
+ id: family.id,
59
+ signature: family.signature,
60
+ navigation: family.navigation,
61
+ routes: records.filter(record => runtimeFamilyByRecord.get(record)?.id === family.id).map(record => record.route).sort(),
62
+ manifest: family.capability,
63
+ requirements: familyRuntimeRequirements(family, base)
64
+ })),
65
+ routes,
66
+ sharedChunks: [...owners].filter(([, routes]) => routes.size > 1).map(([path, routes]) => ({ path, routes: [...routes].sort() })).sort((left, right) => left.path.localeCompare(right.path))
67
+ }
68
+ }
69
+
70
+ function routeRuntimeEdges(record, capability, family, base, navigationAsset) {
71
+ const entries = Object.values(record.entries).map(path => assetPath(base, `assets/${path}`))
72
+ if (navigationAsset) entries.push(navigationAsset)
73
+ if (!family) return { family: null, entries: [...new Set(entries)].sort(), requirements: [] }
74
+ const modules = []
75
+ const add = name => modules.push(runtimeAsset(base, family.id, name))
76
+ if (record.capabilities.hasBehaviors || record.capabilities.navigable) add(record.capabilities.usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js")
77
+ if (record.capabilities.hasBindings) add("kudzu-binding.js")
78
+ if (record.capabilities.hasLists) add("kudzu-list.js")
79
+ if (record.capabilities.hasBindings || record.capabilities.hasLists && family.capability.lists.styleCount) add("kudzu-style.js")
80
+ if (capability.effects.derivedDependencies || record.capabilities.hasLists && family.capability.lists.selectors) add("kudzu-collection-selector.js")
81
+ if (record.capabilities.hasBindings || capability.events.hasNativeHandlers || record.capabilities.hasEffects && family.capability.effects.captures) add("kudzu-serialization.js")
82
+ if (record.capabilities.hasEffects) add("kudzu-effect.js")
83
+ if (capability.events.hasNativeHandlers) add("kudzu-native.js")
84
+ return { family: family.id, entries: [...new Set(entries)].sort(), requirements: [...new Set(modules)].sort() }
85
+ }
86
+
87
+ function familyRuntimeRequirements(family, base) {
88
+ const { bindings, effects, events, lists, runtime } = family.capability
89
+ const names = [runtime.dependency ? "kudzu-deps.js" : "kudzu.js"]
90
+ if (bindings.count || events.hasNativeHandlers || effects.captures) names.push("kudzu-serialization.js")
91
+ if (effects.any) names.push("kudzu-effect.js")
92
+ if (bindings.count || lists.styleCount) names.push("kudzu-style.js")
93
+ if (bindings.count) names.push("kudzu-binding.js")
94
+ if (effects.derivedDependencies || lists.selectors) names.push("kudzu-collection-selector.js")
95
+ if (lists.count) names.push("kudzu-list.js")
96
+ if (events.hasNativeHandlers) names.push("kudzu-native.js")
97
+ return names.map(name => runtimeAsset(base, family.id, name)).sort()
98
+ }
99
+
100
+ const runtimeAsset = (base, id, name) => assetPath(base, `assets/runtime/${id}/${name}`)
101
+
102
+ function outputGraph(metafile, outputDirectory, base) {
103
+ if (!metafile || typeof metafile !== "object" || !metafile.outputs || typeof metafile.outputs !== "object") throw new Error("Invalid esbuild metafile")
104
+ if (typeof outputDirectory !== "string") throw new Error("Route artifact reporting requires an output directory")
105
+ const paths = new Map()
106
+ for (const output of Object.keys(metafile.outputs)) paths.set(resolve(output), outputUrl(resolve(output), outputDirectory, base))
107
+ const graph = new Map()
108
+ for (const [output, metadata] of Object.entries(metafile.outputs)) {
109
+ const absolute = resolve(output)
110
+ const url = paths.get(absolute)
111
+ const imports = (metadata.imports ?? []).filter(entry => !entry.external).map(entry => paths.get(resolve(entry.path)) ?? paths.get(resolve(dirname(absolute), entry.path))).filter(Boolean)
112
+ graph.set(url, [...new Set(imports)].sort())
113
+ }
114
+ return graph
115
+ }
116
+
117
+ function outputUrl(output, outputDirectory, base) {
118
+ const path = relative(outputDirectory, output)
119
+ if (!path || path === ".." || path.startsWith(`..${sep}`)) throw new Error(`Bundled output is outside the build directory: ${output}`)
120
+ return assetPath(base, path.replaceAll(sep, "/"))
121
+ }
122
+
123
+ function closure(entries, graph, requireEntries) {
124
+ const visited = new Set()
125
+ const visit = output => {
126
+ if (visited.has(output)) return
127
+ if (requireEntries && !graph.has(output)) throw new Error(`Bundled handler entry was not emitted: ${output}`)
128
+ visited.add(output)
129
+ for (const imported of graph.get(output) ?? []) visit(imported)
130
+ }
131
+ for (const entry of entries) visit(entry)
132
+ return [...visited].sort()
133
+ }
@@ -0,0 +1,48 @@
1
+ import { createHash } from "node:crypto"
2
+ import { assertRouteBuildRecord } from "./route-build-record.mjs"
3
+ import { planRouteCapabilities } from "./route-capability-planner.mjs"
4
+
5
+ export function capabilitySignature(capability) {
6
+ return createHash("sha256").update(JSON.stringify(capability)).digest("hex")
7
+ }
8
+
9
+ export function planRuntimeFamilies(records, navigationGroups = []) {
10
+ for (const record of records) assertRouteBuildRecord(record)
11
+ const grouped = new Set()
12
+ const scopes = []
13
+ for (const group of navigationGroups) {
14
+ const groupRecords = group.buildRecords ?? group.records ?? []
15
+ if (!groupRecords.length) continue
16
+ for (const record of groupRecords) {
17
+ if (!records.includes(record)) throw new Error(`Navigation runtime family contains an unknown route: ${record.route}`)
18
+ if (grouped.has(record)) throw new Error(`Route belongs to multiple runtime families: ${record.route}`)
19
+ grouped.add(record)
20
+ }
21
+ scopes.push({ navigation: true, records: groupRecords })
22
+ }
23
+ for (const record of records) if (!grouped.has(record) && record.capabilities.hasBehaviors) scopes.push({ navigation: false, records: [record] })
24
+
25
+ const familiesBySignature = new Map()
26
+ const signaturesById = new Map()
27
+ const familyByRecord = new Map()
28
+ for (const scope of scopes) {
29
+ const capability = planRouteCapabilities(scope.records, { navigationRouteCount: scope.navigation ? scope.records.length : 0 })
30
+ const descriptor = { version: 1, navigation: scope.navigation, capability }
31
+ const signature = capabilitySignature(descriptor)
32
+ const id = signature.slice(0, 16)
33
+ const existingSignature = signaturesById.get(id)
34
+ if (existingSignature && existingSignature !== signature) throw new Error(`Runtime family ID collision: ${id}`)
35
+ signaturesById.set(id, signature)
36
+ let family = familiesBySignature.get(signature)
37
+ if (!family) {
38
+ family = { id, signature, navigation: scope.navigation, capability, records: [] }
39
+ familiesBySignature.set(signature, family)
40
+ }
41
+ for (const record of scope.records) {
42
+ family.records.push(record)
43
+ familyByRecord.set(record, family)
44
+ }
45
+ }
46
+ const families = [...familiesBySignature.values()].map(family => ({ ...family, records: [...family.records].sort((left, right) => left.route.localeCompare(right.route)) })).sort((left, right) => left.id.localeCompare(right.id))
47
+ return { families, familyByRecord }
48
+ }
@@ -24,6 +24,7 @@ import { createZustandPass } from "./zustand-pass.mjs"
24
24
 
25
25
  export function createSourceCompiler(project) {
26
26
  const { root, sourceDirectory, pagesDirectory, workDirectory, workerCompiler, modules, counters } = project
27
+ const buildDirectory = project.buildDirectory ?? workDirectory
27
28
  const { ordinaryRuntimeDependencies, resolveSourceImport, runtimeModuleReference } = project.graph
28
29
  const parseSourceFile = (file, source) => modules.read(file, source).sourceFile
29
30
  const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
@@ -3166,7 +3167,7 @@ function clientModulePath(file) {
3166
3167
  }
3167
3168
 
3168
3169
  function compiledPath(file) {
3169
- return join(workDirectory, relative(sourceDirectory, file)).replace(/\.(?:ts|tsx)$/, ".mjs")
3170
+ return join(buildDirectory, relative(sourceDirectory, file)).replace(/\.(?:ts|tsx)$/, ".mjs")
3170
3171
  }
3171
3172
 
3172
3173
  const compileEventCommand = createCommandSpecializer({ isPrimitiveLiteral: isPrimitiveDefaultLiteral })
@@ -130,7 +130,7 @@ export function createWorkerCompiler({
130
130
 
131
131
  const emit = async (references, sourceFiles, assetsDirectory, base, minify) => {
132
132
  const roots = [...new Set(references.map(reference => resolve(sourceDirectory, reference.root)))].sort()
133
- if (!roots.length) return new Map()
133
+ if (!roots.length) return { assets: new Map(), outputs: new Map() }
134
134
  await validateGraphs(roots, sourceFiles)
135
135
  const workerDirectory = resolve(assetsDirectory, "workers")
136
136
  await mkdir(workerDirectory, { recursive: true })
@@ -152,16 +152,35 @@ export function createWorkerCompiler({
152
152
  logLevel: "silent"
153
153
  })
154
154
  const emitted = new Map()
155
+ const outputUrls = new Map(Object.keys(result.metafile.outputs).map(output => {
156
+ const outputFile = resolve(root, output)
157
+ return [outputFile, assetPath(base, relative(resolve(assetsDirectory, ".."), outputFile).replaceAll(sep, "/"))]
158
+ }))
159
+ const outputImports = new Map(Object.entries(result.metafile.outputs).map(([output, metadata]) => {
160
+ const outputFile = resolve(root, output)
161
+ return [outputUrls.get(outputFile), (metadata.imports ?? []).filter(entry => !entry.external).map(entry => outputUrls.get(resolve(root, entry.path)) ?? outputUrls.get(resolve(outputFile, "..", entry.path))).filter(Boolean)]
162
+ }))
163
+ const outputs = new Map()
155
164
  for (const [output, metadata] of Object.entries(result.metafile.outputs)) {
156
165
  if (!metadata.entryPoint) continue
157
166
  const entry = resolve(root, metadata.entryPoint)
158
167
  const rootReferences = references.filter(reference => resolve(sourceDirectory, reference.root) === entry)
159
168
  const outputFile = resolve(root, output)
160
- const url = assetPath(base, relative(resolve(assetsDirectory, ".."), outputFile).replaceAll(sep, "/"))
161
- for (const reference of rootReferences) emitted.set(reference.placeholder, url)
169
+ const url = outputUrls.get(outputFile)
170
+ const closure = new Set()
171
+ const visit = path => {
172
+ if (closure.has(path)) return
173
+ closure.add(path)
174
+ for (const imported of outputImports.get(path) ?? []) visit(imported)
175
+ }
176
+ visit(url)
177
+ for (const reference of rootReferences) {
178
+ emitted.set(reference.placeholder, url)
179
+ outputs.set(reference.placeholder, { entry: url, chunks: [...closure].filter(path => path !== url).sort() })
180
+ }
162
181
  }
163
182
  for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${reference.root}`)
164
- return emitted
183
+ return { assets: emitted, outputs }
165
184
  }
166
185
 
167
186
  return { candidate, emit, rejectConstructions, rejectOrdinaryImports, rewriteEffect }
@@ -64,6 +64,8 @@ export type PageMetadata = {
64
64
  styles?: boolean | string[]
65
65
  base?: string
66
66
  runtimeAsset?: string
67
+ bindingAsset?: string
68
+ listAsset?: string
67
69
  effectAsset?: string
68
70
  nativeAsset?: string
69
71
  paramAsset?: string
@@ -501,10 +501,10 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
501
501
  ? `<script type="module"${capability} src="${escapeAttribute(metadata.paramAsset)}"></script>`
502
502
  : ""
503
503
  const bindingRuntime = renderContext.hasBindings
504
- ? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-binding.js")}"></script>`
504
+ ? `<script type="module"${capability} src="${escapeAttribute(metadata.bindingAsset ?? assetPath(metadata.base, "assets/kudzu-binding.js"))}"></script>`
505
505
  : ""
506
506
  const listRuntime = renderContext.hasLists
507
- ? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-list.js")}"></script>`
507
+ ? `<script type="module"${capability} src="${escapeAttribute(metadata.listAsset ?? assetPath(metadata.base, "assets/kudzu-list.js"))}"></script>`
508
508
  : ""
509
509
  const effectRuntime = renderContext.hasEffects
510
510
  ? `<script type="module"${capability} src="${escapeAttribute(metadata.effectAsset)}"></script>`
@@ -5,7 +5,7 @@ import { extname, join, resolve, sep } from "node:path"
5
5
  import { browserPath, withBase } from "./compiler/path-helpers.mjs"
6
6
  import { stateSchema } from "./dev-state.js"
7
7
 
8
- const devClient = (session, revision, schema) => `<script>(()=>{const show=event=>{let box=document.getElementById("__kudzu_error");if(!box){box=document.createElement("div");box.id="__kudzu_error";box.setAttribute("role","alert");box.setAttribute("aria-live","assertive");box.style.cssText="position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace";const title=document.createElement("strong"),text=document.createElement("pre");title.textContent="Kudzu build error";text.style.whiteSpace="pre-wrap";box.append(title,text);document.body.append(box)}box.querySelector("pre").textContent=event.data};const schema=${inlineJson(schema)},route=location.pathname+location.search+location.hash,urls=[...document.querySelectorAll('script[type="module"][src]')].map(node=>node.src).filter(url=>/\/assets\/kudzu(?:-(?:binding|list|native))?\.js$/.test(new URL(url).pathname));const devImport=import("/__kudzu_dev.js"),runtimeImports=Promise.allSettled(urls.map(url=>import(url)));const ready=(async()=>{const dev=await devImport,modules=await runtimeImports,runtime=modules.find(result=>result.status==="fulfilled"&&result.value.browserState instanceof Map&&typeof result.value.commitDom==="function")?.value;try{dev.restoreState(sessionStorage,route,runtime?.browserState,schema,runtime?.commitDom)}catch{}return{dev,runtime}})().catch(()=>({}));const events=new EventSource("/__kudzu_reload?session=${session}&revision=${revision}");let reloading=false;events.addEventListener("reload",async()=>{if(reloading)return;reloading=true;try{const{dev,runtime}=await ready;dev?.snapshotState(sessionStorage,route,runtime?.browserState,schema)}catch{}location.reload()});events.addEventListener("build-error",show)})()</script>`
8
+ const devClient = (session, revision, schema) => `<script>(()=>{const show=event=>{let box=document.getElementById("__kudzu_error");if(!box){box=document.createElement("div");box.id="__kudzu_error";box.setAttribute("role","alert");box.setAttribute("aria-live","assertive");box.style.cssText="position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace";const title=document.createElement("strong"),text=document.createElement("pre");title.textContent="Kudzu build error";text.style.whiteSpace="pre-wrap";box.append(title,text);document.body.append(box)}box.querySelector("pre").textContent=event.data};const schema=${inlineJson(schema)},route=location.pathname+location.search+location.hash,urls=[...document.querySelectorAll('script[type="module"][src]')].map(node=>node.src).filter(url=>/\/assets\/runtime\/[a-f0-9]+\/kudzu(?:-deps|-binding|-list|-native)?\.js$/.test(new URL(url).pathname));const devImport=import("/__kudzu_dev.js"),runtimeImports=Promise.allSettled(urls.map(url=>import(url)));const ready=(async()=>{const dev=await devImport,modules=await runtimeImports,runtime=modules.find(result=>result.status==="fulfilled"&&result.value.browserState instanceof Map&&typeof result.value.commitDom==="function")?.value;try{dev.restoreState(sessionStorage,route,runtime?.browserState,schema,runtime?.commitDom)}catch{}return{dev,runtime}})().catch(()=>({}));const events=new EventSource("/__kudzu_reload?session=${session}&revision=${revision}");let reloading=false;events.addEventListener("reload",async()=>{if(reloading)return;reloading=true;try{const{dev,runtime}=await ready;dev?.snapshotState(sessionStorage,route,runtime?.browserState,schema)}catch{}location.reload()});events.addEventListener("build-error",show)})()</script>`
9
9
 
10
10
  export function parseDevPort(value) {
11
11
  if (value === undefined || value.trim() === "") return 3000
@@ -85,6 +85,7 @@ export async function startDevServer({ build, port, host, base, sourceDirectory,
85
85
  let rebuilding = false
86
86
  let pending = false
87
87
  let changedFile
88
+ const changedFiles = new Set()
88
89
  const rebuild = async () => {
89
90
  if (rebuilding) {
90
91
  pending = true
@@ -93,13 +94,16 @@ export async function startDevServer({ build, port, host, base, sourceDirectory,
93
94
  rebuilding = true
94
95
  do {
95
96
  pending = false
97
+ const changes = [...changedFiles]
98
+ changedFiles.clear()
96
99
  try {
97
- await build({ quiet: true, minify: false })
100
+ await build({ changedFiles: changes, quiet: true, minify: false })
98
101
  buildError = undefined
99
102
  revision++
100
103
  console.log(`Rebuilt after ${changedFile ?? "source change"}`)
101
104
  for (const client of clients) sendEvent(client, "reload")
102
105
  } catch (error) {
106
+ for (const file of changes) changedFiles.add(file)
103
107
  buildError = errorText(error)
104
108
  console.error(error)
105
109
  for (const client of clients) sendEvent(client, "build-error", buildError)
@@ -110,6 +114,7 @@ export async function startDevServer({ build, port, host, base, sourceDirectory,
110
114
  const watcher = watch(sourceDirectory, { recursive: true })
111
115
  for await (const event of watcher) {
112
116
  changedFile = event.filename
117
+ changedFiles.add(event.filename)
113
118
  clearTimeout(timer)
114
119
  timer = setTimeout(rebuild, 80)
115
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.53",
3
+ "version": "0.8.56",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",