@kudzujs/core 0.8.52 → 0.8.55

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.
@@ -8,9 +8,11 @@ import { generateListRuntime } from "./compiler/list-runtime-codegen.mjs"
8
8
  import { assetPath, browserPath, relativeModulePath, withBase } from "./compiler/path-helpers.mjs"
9
9
  import { createProjectSession } from "./compiler/project-session.mjs"
10
10
  import { createRouteBuildRecord, planRouteArtifacts } from "./compiler/route-build-record.mjs"
11
+ import { createRouteArtifactReport } from "./compiler/route-artifact-report.mjs"
12
+ import { planRuntimeFamilies } from "./compiler/runtime-family-planner.mjs"
11
13
  import { createSourceCompiler } from "./compiler/source-compiler.mjs"
12
14
  import { createParamCodegen } from "./compiler/param-codegen.mjs"
13
- import { planRouteCapabilities, usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
15
+ import { usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
14
16
  import { generateBindingRuntime, generateCoreRuntime, generateEffectRuntime, generateNativeRuntime, generateNavigationRuntime, specializeRuntime } from "./compiler/runtime-codegen.mjs"
15
17
  import { renderPage } from "./core.mjs"
16
18
  import { parseDevHost, parseDevPort, startDevServer } from "./dev-server.mjs"
@@ -62,9 +64,10 @@ async function buildInto(project, outputDirectory, { minify }) {
62
64
  const config = await loadConfig(root)
63
65
  const base = normalizeBase(config.base)
64
66
  const configuredStyles = normalizeStyles(config.styles, base, project)
67
+ const globalStyleUrls = [...new Set(configuredStyles.urls)]
68
+ const globalStyleUrlSet = new Set(globalStyleUrls)
65
69
  const publicDirectory = normalizePublicDirectory(config.publicDir, project)
66
70
  const navigationGroups = normalizeNavigation(config.navigation)
67
- const navigationRoutes = navigationGroups.flatMap(group => group.routes)
68
71
  const navigationByRoute = new Map(navigationGroups.flatMap(group => group.routes.map(route => [route, group])))
69
72
  for (const group of navigationGroups) {
70
73
  group.assetPath = assetPath(base, `assets/${group.assetName}`)
@@ -72,6 +75,7 @@ async function buildInto(project, outputDirectory, { minify }) {
72
75
  group.layoutId = `l-${group.id}`
73
76
  group.records = []
74
77
  group.routeRecords = []
78
+ group.buildRecords = []
75
79
  group.hasEffects = false
76
80
  group.hasParams = false
77
81
  }
@@ -82,7 +86,6 @@ async function buildInto(project, outputDirectory, { minify }) {
82
86
  const projectFiles = await walk(sourceDirectory)
83
87
  const allSourceFiles = projectFiles.filter(file => /\.(?:ts|tsx)$/.test(file) && !file.endsWith(".d.ts")).sort()
84
88
  const configuredStyleSources = new Set(configuredStyles.sources.map(style => style.source))
85
- const discoveredCssFiles = projectFiles.filter(file => file.toLowerCase().endsWith(".css") && !configuredStyleSources.has(file)).sort()
86
89
  if (!allSourceFiles.length) throw new Error("No TypeScript files found in src/")
87
90
  const allSourceFileSet = new Set(allSourceFiles)
88
91
  const sourceIndex = project.sourceIndex
@@ -93,7 +96,8 @@ async function buildInto(project, outputDirectory, { minify }) {
93
96
  const sourceFileSet = project.sourceFiles
94
97
  for (const file of sourceFiles) sourceFileSet.add(file)
95
98
  const staticFiles = await safeStaticFiles(projectFiles)
96
- const cssFiles = orderSourceStyles(discoveredCssFiles, sourceFiles, sourceIndex, staticFiles)
99
+ const stylesByPage = new Map(pageFiles.map(file => [file, orderSourceStyles([file], sourceFiles, sourceIndex, staticFiles).filter(style => !configuredStyleSources.has(style))]))
100
+ const cssFiles = [...new Set([...stylesByPage.values()].flat())]
97
101
  const importedAssets = new Set()
98
102
  const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base, project)
99
103
 
@@ -114,7 +118,8 @@ async function buildInto(project, outputDirectory, { minify }) {
114
118
  return effect.workers.map(worker => ({ ...worker, module: assetPath(base, `assets/${result.handlerModule.path}`), handler: handler.exportName }))
115
119
  }))
116
120
 
117
- const routeRecords = []
121
+ let routeRecords = []
122
+ const routeDrafts = []
118
123
  const routeEntryTransforms = new Map()
119
124
  const routeEntrySources = new Map()
120
125
  const routeEntryPaths = new Map()
@@ -122,13 +127,14 @@ async function buildInto(project, outputDirectory, { minify }) {
122
127
  const emittedRoutes = new Set()
123
128
  const emittedApplicationRoutes = new Set()
124
129
  const emittedNavigationRecords = []
125
- const styleUrls = [...new Set([
126
- ...cssFiles.map(file => assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)),
127
- ...configuredStyles.urls
128
- ])]
130
+ const navigationAssets = new Map()
129
131
  const runtimePlaceholder = `/__kudzu_runtime_${randomUUID()}.js`
132
+ const bindingPlaceholder = `/__kudzu_binding_${randomUUID()}.js`
133
+ const listPlaceholder = `/__kudzu_list_${randomUUID()}.js`
130
134
 
131
135
  for (const pageFile of pageFiles) {
136
+ const sourceStyleUrls = stylesByPage.get(pageFile).map(file => assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)).filter(url => !globalStyleUrlSet.has(url))
137
+ const styleUrls = [...sourceStyleUrls, ...globalStyleUrls]
132
138
  const compiledFile = compiledPath(pageFile)
133
139
  const module = await import(`${pathToFileURL(compiledFile).href}?v=${Date.now()}`)
134
140
  if (typeof module.default !== "function") throw new Error(`${relative(root, pageFile)} must export a default component`)
@@ -156,6 +162,7 @@ async function buildInto(project, outputDirectory, { minify }) {
156
162
  const pageMetadata = await resolveDocumentMetadata(module.metadata, metadataContext, `${relative(root, pageFile)} metadata`)
157
163
  const navigationGroup = navigationByRoute.get(applicationRoute)
158
164
  const navigable = Boolean(navigationGroup)
165
+ if (navigationGroup) navigationAssets.set(routePath, navigationGroup.assetPath)
159
166
  const effectPath = `effects/${route ? `${route}/index` : "index"}.js`
160
167
  const nativePath = `native/${route ? `${route}/index` : "index"}.js`
161
168
  const paramPath = `params/${route ? `${route}/index` : "index"}.js`
@@ -179,8 +186,11 @@ async function buildInto(project, outputDirectory, { minify }) {
179
186
  ...configuredMetadata,
180
187
  ...pageMetadata,
181
188
  styles: styleUrls.length ? styleUrls : false,
189
+ managedStyles: navigable ? sourceStyleUrls : [],
182
190
  base,
183
191
  runtimeAsset: runtimePlaceholder,
192
+ bindingAsset: bindingPlaceholder,
193
+ listAsset: listPlaceholder,
184
194
  effectAsset: assetPath(base, `assets/${effectPath}`),
185
195
  nativeAsset: assetPath(base, `assets/${nativePath}`),
186
196
  paramAsset: assetPath(base, `assets/${paramPath}`),
@@ -192,30 +202,16 @@ async function buildInto(project, outputDirectory, { minify }) {
192
202
  navigationGroup.hasParams ||= result.hasParams
193
203
  }
194
204
  const usesDependencyRuntime = usesRouteDependencyRuntime({ plan: result.plan, navigable, hasBindings: result.hasBindings, hasLists: result.hasLists })
195
- const routeRuntimeName = usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
196
205
  const plan = { route: routePath, ...result.plan }
197
- const entries = {}
198
- let html = inlineQueryFormCarry(result.html, plan)
199
- if (result.hasParams) {
200
- const entry = retainRouteEntry(paramPath, output => printParamEntry(runtimeSchema, plan.params, plan.searchParams, plan.searchParamsWritable, output, join(outputDirectory, "assets"), base, routeRuntimeName, navigable), routeEntrySources, routeEntryPaths, outputDirectory)
201
- entries.param = entry.path
202
- html = html.replaceAll(assetPath(base, `assets/${paramPath}`), assetPath(base, `assets/${entry.path}`))
206
+ const entries = {
207
+ ...(result.hasParams ? { param: paramPath } : {}),
208
+ ...(result.hasEffects ? { effect: effectPath } : {}),
209
+ ...(plan.events.some(event => event.native) ? { native: nativePath } : {})
203
210
  }
204
- if (result.hasEffects) {
205
- const entry = retainRouteEntry(effectPath, output => printEffectEntry(runtimeEffects(plan.effects, navigable), output, handlerModules, join(outputDirectory, "assets"), base, entries.param, routeRuntimeName, navigable), routeEntrySources, routeEntryPaths, outputDirectory)
206
- entries.effect = entry.path
207
- html = html.replaceAll(assetPath(base, `assets/${effectPath}`), assetPath(base, `assets/${entry.path}`))
208
- }
209
- if (plan.events.some(event => event.native)) {
210
- const modules = [...new Set(plan.events.filter(event => event.native).map(event => event.native.module))]
211
- const entry = retainRouteEntry(nativePath, () => printNativeEntrySource(modules, base), routeEntrySources, routeEntryPaths, outputDirectory)
212
- entries.native = entry.path
213
- html = html.replaceAll(assetPath(base, `assets/${nativePath}`), assetPath(base, `assets/${entry.path}`))
214
- }
215
- routeRecords.push(createRouteBuildRecord({
211
+ const record = createRouteBuildRecord({
216
212
  route: routePath,
217
213
  output: route,
218
- html,
214
+ html: inlineQueryFormCarry(result.html, plan),
219
215
  plan,
220
216
  handlerReferences: result.handlerReferences,
221
217
  styles: styleUrls,
@@ -232,7 +228,10 @@ async function buildInto(project, outputDirectory, { minify }) {
232
228
  },
233
229
  entries,
234
230
  runtimeSchema
235
- }))
231
+ })
232
+ routeRecords.push(record)
233
+ if (navigationGroup) navigationGroup.buildRecords.push(record)
234
+ routeDrafts.push({ record, result, runtimeSchema, navigationGroup, effectPath, nativePath, paramPath })
236
235
  }
237
236
  }
238
237
 
@@ -244,12 +243,65 @@ async function buildInto(project, outputDirectory, { minify }) {
244
243
  group.records.sort((left, right) => (right.segments?.filter(segment => segment !== null).length ?? 0) - (left.segments?.filter(segment => segment !== null).length ?? 0) || left.id.localeCompare(right.id))
245
244
  }
246
245
 
246
+ const runtimePlan = planRuntimeFamilies(routeRecords, navigationGroups)
247
+ for (const group of navigationGroups) {
248
+ group.runtimeFamily = runtimePlan.familyByRecord.get(group.buildRecords[0])
249
+ group.assetPath = assetPath(base, `assets/runtime/${group.runtimeFamily.id}/${group.assetName}`)
250
+ }
251
+ const runtimeFamilyByRecord = new Map()
252
+ routeRecords = routeDrafts.map(draft => {
253
+ const { record, result, runtimeSchema, navigationGroup, effectPath, nativePath, paramPath } = draft
254
+ const family = runtimePlan.familyByRecord.get(record)
255
+ if (record.capabilities.hasBehaviors && !family) throw new Error(`Interactive route has no runtime family: ${record.route}`)
256
+ const runtimeDirectory = family ? join(outputDirectory, "assets", "runtime", family.id) : undefined
257
+ const routeRuntimeName = record.capabilities.usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
258
+ const entries = {}
259
+ let html = record.html
260
+ if (record.capabilities.hasParams) {
261
+ const entry = retainRouteEntry(paramPath, output => printParamEntry(runtimeSchema, record.plan.params, record.plan.searchParams, record.plan.searchParamsWritable, output, runtimeDirectory, base, routeRuntimeName, record.capabilities.navigable), routeEntrySources, routeEntryPaths, outputDirectory)
262
+ entries.param = entry.path
263
+ html = html.replaceAll(assetPath(base, `assets/${paramPath}`), assetPath(base, `assets/${entry.path}`))
264
+ }
265
+ if (record.capabilities.hasEffects) {
266
+ const entry = retainRouteEntry(effectPath, output => printEffectEntry(runtimeEffects(record.plan.effects, record.capabilities.navigable), output, handlerModules, join(outputDirectory, "assets"), base, entries.param, routeRuntimeName, record.capabilities.navigable, runtimeDirectory), routeEntrySources, routeEntryPaths, outputDirectory)
267
+ entries.effect = entry.path
268
+ html = html.replaceAll(assetPath(base, `assets/${effectPath}`), assetPath(base, `assets/${entry.path}`))
269
+ }
270
+ if (record.plan.events.some(event => event.native)) {
271
+ const modules = [...new Set(record.plan.events.filter(event => event.native).map(event => event.native.module))]
272
+ const nativeRuntime = assetPath(base, `assets/runtime/${family.id}/kudzu-native.js`)
273
+ const entry = retainRouteEntry(nativePath, () => printNativeEntrySource(modules, nativeRuntime), routeEntrySources, routeEntryPaths, outputDirectory)
274
+ entries.native = entry.path
275
+ html = html.replaceAll(assetPath(base, `assets/${nativePath}`), assetPath(base, `assets/${entry.path}`))
276
+ }
277
+ if (family) {
278
+ html = html.replaceAll(runtimePlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/${routeRuntimeName}`)))
279
+ html = html.replaceAll(bindingPlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/kudzu-binding.js`)))
280
+ html = html.replaceAll(listPlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/kudzu-list.js`)))
281
+ }
282
+ if (navigationGroup) html = html.replaceAll(escapeAttribute(navigationAssets.get(record.route)), escapeAttribute(navigationGroup.assetPath))
283
+ const finalRecord = createRouteBuildRecord({
284
+ route: record.route,
285
+ output: record.output,
286
+ html,
287
+ plan: record.plan,
288
+ handlerReferences: result.handlerReferences,
289
+ styles: record.artifacts.styles,
290
+ capabilities: record.capabilities,
291
+ entries,
292
+ runtimeSchema
293
+ })
294
+ if (family) runtimeFamilyByRecord.set(finalRecord, family)
295
+ if (navigationGroup) navigationAssets.set(finalRecord.route, navigationGroup.assetPath)
296
+ return finalRecord
297
+ })
298
+
247
299
  const assetsDirectory = join(outputDirectory, "assets")
248
300
  await mkdir(assetsDirectory, { recursive: true })
249
301
  const { handlerModules: emittedHandlerModules, workerReferences: renderedWorkerReferences, styles: renderedStyles } = planRouteArtifacts(routeRecords, handlerModules, workerReferences, module => assetPath(base, `assets/${module.path}`))
250
302
  const renderedStyleUrls = new Set(renderedStyles)
251
303
  if (renderedWorkerReferences.length && await exists(join(publicDirectory, "assets", "workers"))) throw new Error("public/assets/workers collides with Kudzu's generated Worker asset namespace")
252
- const workerAssets = await project.workerCompiler.emit(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
304
+ const { assets: workerAssets, outputs: workerOutputs } = await project.workerCompiler.emit(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
253
305
  for (const module of emittedHandlerModules) {
254
306
  for (const reference of workerReferences) {
255
307
  if (reference.module !== assetPath(base, `assets/${module.path}`)) continue
@@ -259,63 +311,55 @@ async function buildInto(project, outputDirectory, { minify }) {
259
311
  if (module.code.includes("/__kudzu_worker_")) throw new Error(`Worker URL placeholder survived in ${module.path}`)
260
312
  }
261
313
  const plans = routeRecords.map(record => record.plan)
262
- const capabilityIR = planRouteCapabilities(routeRecords, { navigationRouteCount: navigationRoutes.length })
263
- const {
264
- routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount, dependencyStateSeeds: dependencyStateSeedCount },
265
- events: { command: commandEvents, hasNativeHandlers },
266
- bindings: { count: bindingCount },
267
- lists,
268
- effects: { any: hasEffects, derivedDependencies: hasDerivedEffectDependencies, captures: hasEffectCaptures },
269
- captures: { nestedState: hasNestedStateCaptures, setter: hasSetterCaptures },
270
- runtime: { shared: hasSharedRuntime, dependency: hasDependencyRuntime }
271
- } = capabilityIR
272
- const runtimeName = usesDependencyRuntime => usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
314
+ const behaviorCount = routeRecords.filter(record => record.capabilities.hasBehaviors).length
273
315
  for (let offset = 0; offset < routeRecords.length; offset += 64) {
274
316
  await Promise.all(routeRecords.slice(offset, offset + 64).map(async record => {
275
317
  const routeDirectory = join(outputDirectory, record.output)
276
318
  await mkdir(routeDirectory, { recursive: true })
277
- const html = preloadModules(record.html.replace(runtimePlaceholder, escapeAttribute(assetPath(base, `assets/${runtimeName(record.capabilities.usesDependencyRuntime)}`))))
278
- await writeFile(join(routeDirectory, "index.html"), html)
319
+ if ([runtimePlaceholder, bindingPlaceholder, listPlaceholder].some(placeholder => record.html.includes(placeholder))) throw new Error(`Runtime family placeholder survived in ${record.route}`)
320
+ await writeFile(join(routeDirectory, "index.html"), preloadModules(record.html))
279
321
  }))
280
322
  }
281
- if (navigationRoutes.length || behaviorCount && (hasSharedRuntime || regularBehaviorCount)) {
282
- const runtimeFile = hasSharedRuntime ? "./shared-runtime.js" : "./runtime.js"
283
- const runtime = generateCoreRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), capabilityIR)
284
- await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
285
- }
286
- if (hasDependencyRuntime) {
287
- const runtime = specializeRuntime(await readFile(new URL("./dependency-runtime.js", import.meta.url), "utf8"), commandEvents, dependencyStateSeedCount > 0)
288
- await writeJavaScript(join(assetsDirectory, "kudzu-deps.js"), runtime, minify)
289
- }
290
- if (bindingCount || hasNativeHandlers || hasEffectCaptures) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify, {
291
- "globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures),
292
- "globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
293
- })
294
- if (hasEffects) {
295
- const generated = generateEffectRuntime(await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8"), capabilityIR)
296
- await writeBundledJavaScript(join(assetsDirectory, "kudzu-effect.js"), generated.source, minify, generated.define)
297
- }
298
- if (bindingCount || lists.styleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
299
- if (bindingCount) {
300
- const generated = generateBindingRuntime(await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"), capabilityIR, navigationRoutes.length > 0)
301
- await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), generated.source, minify, generated.define)
302
- }
303
- if (hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
304
- if (lists.count) {
305
- if (lists.selectors && !hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
306
- const generated = generateListRuntime(await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"), capabilityIR)
307
- await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), generated.source, minify, generated.define)
308
- if (lists.selectors && !hasDerivedEffectDependencies) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
309
- }
310
- if (hasNativeHandlers) {
311
- const generated = generateNativeRuntime(await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"), capabilityIR)
312
- await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), generated.source, minify, generated.define)
313
- for (const [path, source] of routeEntrySources) if (path.startsWith("native/")) await writeRetainedRouteEntry(path, source, assetsDirectory, minify, routeEntryTransforms)
323
+ for (const family of runtimePlan.families) {
324
+ const capabilityIR = family.capability
325
+ const { bindings, captures, effects, events, lists, routes, runtime } = capabilityIR
326
+ const familyDirectory = join(assetsDirectory, "runtime", family.id)
327
+ await mkdir(familyDirectory, { recursive: true })
328
+ if (runtime.dependency) {
329
+ const source = await readFile(new URL("./dependency-runtime.js", import.meta.url), "utf8")
330
+ await writeJavaScript(join(familyDirectory, "kudzu-deps.js"), specializeRuntime(source, events.command, routes.dependencyStateSeeds > 0), minify)
331
+ } else {
332
+ const source = await readFile(new URL(runtime.shared ? "./shared-runtime.js" : "./runtime.js", import.meta.url), "utf8")
333
+ await writeJavaScript(join(familyDirectory, "kudzu.js"), generateCoreRuntime(source, capabilityIR), minify)
334
+ }
335
+ if (bindings.count || events.hasNativeHandlers || effects.captures) await writeJavaScript(join(familyDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify, {
336
+ "globalThis.__KUDZU_CAPTURE_STATE__": String(captures.nestedState),
337
+ "globalThis.__KUDZU_CAPTURE_SETTER__": String(captures.setter)
338
+ })
339
+ if (effects.any) {
340
+ const generated = generateEffectRuntime(await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8"), capabilityIR)
341
+ await writeBundledJavaScript(join(familyDirectory, "kudzu-effect.js"), generated.source, minify, generated.define)
342
+ }
343
+ if (bindings.count || lists.styleCount) await writeJavaScript(join(familyDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
344
+ if (bindings.count) {
345
+ const generated = generateBindingRuntime(await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"), capabilityIR, family.navigation)
346
+ await writeBundledJavaScript(join(familyDirectory, "kudzu-binding.js"), generated.source, minify, generated.define)
347
+ }
348
+ if (effects.derivedDependencies || lists.selectors) await writeJavaScript(join(familyDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
349
+ if (lists.count) {
350
+ const generated = generateListRuntime(await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"), capabilityIR)
351
+ await writeBundledJavaScript(join(familyDirectory, "kudzu-list.js"), generated.source, minify, generated.define)
352
+ }
353
+ if (events.hasNativeHandlers) {
354
+ const generated = generateNativeRuntime(await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"), capabilityIR)
355
+ await writeJavaScript(join(familyDirectory, "kudzu-native.js"), generated.source, minify, generated.define)
356
+ }
314
357
  }
358
+ for (const [path, source] of routeEntrySources) if (path.startsWith("native/")) await writeRetainedRouteEntry(path, source, assetsDirectory, minify, routeEntryTransforms)
315
359
  if (navigationGroups.length) {
316
360
  const navigationSource = await readFile(new URL("./navigation-runtime.js", import.meta.url), "utf8")
317
361
  for (const group of navigationGroups) {
318
- await writeJavaScript(join(assetsDirectory, group.assetName), generateNavigationRuntime(navigationSource, group), minify)
362
+ await writeJavaScript(join(assetsDirectory, "runtime", group.runtimeFamily.id, group.assetName), generateNavigationRuntime(navigationSource, group), minify)
319
363
  }
320
364
  }
321
365
  for (const handlerModule of emittedHandlerModules) {
@@ -332,8 +376,9 @@ async function buildInto(project, outputDirectory, { minify }) {
332
376
  await mkdir(dirname(output), { recursive: true })
333
377
  await writeJavaScript(output, module.code, minify)
334
378
  }
379
+ let handlerMetafile
335
380
  if (clientModules.length || emittedHandlerModules.some(module => module.hasPackageImports)) {
336
- await bundle({
381
+ const result = await bundle({
337
382
  entryPoints: emittedHandlerModules.map(module => join(assetsDirectory, module.path)),
338
383
  outbase: join(assetsDirectory, "handlers"),
339
384
  outdir: join(assetsDirectory, "handlers"),
@@ -346,22 +391,29 @@ async function buildInto(project, outputDirectory, { minify }) {
346
391
  target: "es2022",
347
392
  minify,
348
393
  legalComments: "none",
394
+ metafile: true,
349
395
  logLevel: "silent"
350
396
  })
397
+ handlerMetafile = result.metafile
351
398
  await rm(join(assetsDirectory, "modules"), { recursive: true, force: true })
352
399
  }
353
400
  const sortedRewrites = rewrites.sort((left, right) => runtimeSpecificity(right) - runtimeSpecificity(left) || left.pattern.localeCompare(right.pattern))
354
401
  await writeFile(join(workDirectory, "kudzu-plan.json"), JSON.stringify({ routes: plans, rewrites: sortedRewrites }, null, 2))
402
+ const artifacts = createRouteArtifactReport(routeRecords, { base, handlerMetafile, outputDirectory, navigationAssets, runtimeFamilies: runtimePlan.families, runtimeFamilyByRecord, workerReferences: renderedWorkerReferences, workerOutputs })
403
+ await writeFile(join(workDirectory, "kudzu-artifacts.json"), JSON.stringify(artifacts, null, 2))
404
+ const emittedCssFiles = new Set()
355
405
  for (const file of cssFiles.filter(file => renderedStyleUrls.has(assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)))) {
356
406
  const output = join(assetsDirectory, relative(sourceDirectory, file))
357
407
  await mkdir(dirname(output), { recursive: true })
358
408
  await writeFile(output, cssOutputs.get(file))
409
+ emittedCssFiles.add(file)
359
410
  }
360
411
  for (const file of [...importedAssets].sort()) {
361
- if (cssOutputs.has(file)) continue
362
412
  const output = join(assetsDirectory, relative(sourceDirectory, file))
363
413
  await mkdir(dirname(output), { recursive: true })
364
- await writeFile(output, await readFile(file))
414
+ if (cssOutputs.has(file)) {
415
+ if (!emittedCssFiles.has(file)) await writeFile(output, cssOutputs.get(file))
416
+ } else await writeFile(output, await readFile(file))
365
417
  }
366
418
  for (const style of configuredStyles.sources.filter(style => renderedStyleUrls.has(withBase(base, style.output)))) {
367
419
  let css = await readFile(style.source, "utf8")
@@ -380,7 +432,7 @@ async function buildInto(project, outputDirectory, { minify }) {
380
432
  }
381
433
  if (config.afterBuild !== undefined) {
382
434
  if (typeof config.afterBuild !== "function") throw new Error("kudzu.config afterBuild must be a function")
383
- await config.afterBuild({ root, outDir: outputDirectory, sourceDir: sourceDirectory, base, routes: plans.map(plan => plan.route), plans, rewrites: sortedRewrites })
435
+ await config.afterBuild({ root, outDir: outputDirectory, sourceDir: sourceDirectory, base, routes: plans.map(plan => plan.route), plans, rewrites: sortedRewrites, artifacts })
384
436
  }
385
437
 
386
438
  return { result: { sourceResults }, pageCount: plans.length, behaviorCount }
@@ -485,10 +537,9 @@ function inlineQueryFormCarry(html, plan) {
485
537
  return html.replace("</body>", `${script}</body>`)
486
538
  }
487
539
 
488
- function printNativeEntrySource(modules, base) {
540
+ function printNativeEntrySource(modules, runtime) {
489
541
  const imports = modules.map((module, index) => `import * as __kNativeModule${index} from ${JSON.stringify(module)}`).join("\n")
490
542
  const registrations = modules.map((module, index) => `[${JSON.stringify(module)}, __kNativeModule${index}]`).join(",")
491
- const runtime = assetPath(base, "assets/kudzu-native.js")
492
543
  return `import { registerNativeModules } from ${JSON.stringify(runtime)}\n${imports}\nregisterNativeModules([${registrations}])`
493
544
  }
494
545
 
@@ -541,7 +592,7 @@ async function writeBundledJavaScript(file, source, minify, define) {
541
592
  stdin: { contents: source, resolveDir: dirname(file), sourcefile: file },
542
593
  bundle: true,
543
594
  write: false,
544
- external: ["./kudzu.js", "./kudzu-binding.js", "./kudzu-serialization.js", "./kudzu-style.js"],
595
+ external: ["./kudzu.js", "./kudzu-binding.js", "./kudzu-collection-selector.js", "./kudzu-serialization.js", "./kudzu-style.js"],
545
596
  define,
546
597
  format: "esm",
547
598
  target: "es2022",
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path"
2
2
 
3
3
  export function createEffectCodegen({ assetPath, inlineJson, relativeModulePath }) {
4
- function printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName) {
4
+ function printEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base, paramPath, runtimeName) {
5
5
  const hasCleanup = effects.some(effect => effect.cleanup)
6
6
  const hasDependencies = effects.some(effect => effect.dependencies?.length || effect.itemDependencies?.length)
7
7
  const hasOwners = effects.some(effect => effect.owner)
@@ -14,10 +14,10 @@ function printEffectEntry(effects, output, handlerModules, assetsDirectory, base
14
14
  })
15
15
  const imports = [
16
16
  hasCleanup || hasDependencies || hasOwners
17
- ? `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}\nconst { browserState, commitDom } = __kRuntime`
18
- : `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}`,
19
- `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
20
- ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
17
+ ? `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, runtimeName)))}\nconst { browserState, commitDom } = __kRuntime`
18
+ : `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, runtimeName)))}`,
19
+ `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-effect.js")))}`,
20
+ ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-collection-selector.js")))}`] : []),
21
21
  ...(paramPath ? [`import ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, paramPath)))}`] : []),
22
22
  ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
23
23
  ]
@@ -178,7 +178,7 @@ addEventListener("pagehide", event => {
178
178
  })`
179
179
  }
180
180
 
181
- function printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
181
+ function printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base) {
182
182
  const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
183
183
  const moduleUrls = [...new Set(effects.map(effect => effect.module))]
184
184
  const modules = moduleUrls.map(url => {
@@ -187,9 +187,9 @@ function printNavigableEffectEntry(effects, output, handlerModules, assetsDirect
187
187
  return module
188
188
  })
189
189
  const imports = [
190
- `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
191
- `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
192
- ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
190
+ `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu.js")))}`,
191
+ `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-effect.js")))}`,
192
+ ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-collection-selector.js")))}`] : []),
193
193
  ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
194
194
  ]
195
195
  const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
@@ -300,7 +300,7 @@ ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState
300
300
  }`
301
301
  }
302
302
 
303
- function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
303
+ function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base) {
304
304
  const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
305
305
  const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
306
306
  const moduleUrls = [...new Set(effects.map(effect => effect.module))]
@@ -310,9 +310,9 @@ function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsD
310
310
  return module
311
311
  })
312
312
  const imports = [
313
- `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
314
- `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
315
- ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
313
+ `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu.js")))}`,
314
+ `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-effect.js")))}`,
315
+ ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-collection-selector.js")))}`] : []),
316
316
  ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
317
317
  ]
318
318
  const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
@@ -876,9 +876,9 @@ async function invokeCleanup() {
876
876
  }${disposal}`
877
877
  }
878
878
 
879
- return (effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName, navigable) => navigable
879
+ return (effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName, navigable, runtimeDirectory = assetsDirectory) => navigable
880
880
  ? effects.some(effect => effect.owner)
881
- ? printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base)
882
- : printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base)
883
- : printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName)
881
+ ? printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base)
882
+ : printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base)
883
+ : printEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base, paramPath, runtimeName)
884
884
  }
@@ -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
+ }