@kudzujs/core 0.8.21 → 0.8.23

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.
@@ -0,0 +1,29 @@
1
+ import { dirname, extname, join, relative, resolve } from "node:path"
2
+ import ts from "typescript"
3
+
4
+ const root = process.cwd()
5
+
6
+ export function resolveSourceImport(importer, specifier, sourceFiles) {
7
+ const base = resolve(dirname(importer), specifier)
8
+ const extension = extname(base)
9
+ const stem = /\.(?:js|jsx|ts|tsx)$/.test(extension) ? base.slice(0, -extension.length) : base
10
+ const candidates = extension === ".ts" || extension === ".tsx"
11
+ ? [base]
12
+ : [`${stem}.ts`, `${stem}.tsx`, join(stem, "index.ts"), join(stem, "index.tsx")]
13
+ const matches = candidates.filter(candidate => sourceFiles.has(candidate))
14
+ if (matches.length !== 1) throw new Error(`${relative(root, importer)} Relative import ${JSON.stringify(specifier)} must resolve to one TypeScript file in src/`)
15
+ return matches[0]
16
+ }
17
+
18
+ export function runtimeModuleReference(node) {
19
+ if (ts.isExportDeclaration(node)) return !node.isTypeOnly && (!node.exportClause || !ts.isNamedExports(node.exportClause) || node.exportClause.elements.some(entry => !entry.isTypeOnly))
20
+ const clause = node.importClause
21
+ if (!clause) return true
22
+ if (clause.isTypeOnly) return false
23
+ if (clause.name || clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) return true
24
+ return clause.namedBindings?.elements.some(entry => !entry.isTypeOnly) ?? false
25
+ }
26
+
27
+ export function parseSourceFile(file, source) {
28
+ return ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
29
+ }
@@ -4,11 +4,12 @@ import { dirname, relative, resolve, sep } from "node:path"
4
4
  import { build as bundle } from "esbuild"
5
5
  import ts from "typescript"
6
6
  import { containsJsx, isUnshadowedGlobal, nearestFunction, sourceNodeError } from "./ast-helpers.mjs"
7
+ import { assetPath } from "./path-helpers.mjs"
8
+ import { parseSourceFile, resolveSourceImport, runtimeModuleReference } from "./source-graph.mjs"
7
9
 
8
10
  export function createWorkerCompiler({
9
11
  root,
10
12
  sourceDirectory,
11
- outputDirectory,
12
13
  assetPath,
13
14
  parseSourceFile,
14
15
  resolveSourceImport,
@@ -158,7 +159,7 @@ export function createWorkerCompiler({
158
159
  const entry = resolve(root, metadata.entryPoint)
159
160
  const rootReferences = references.filter(reference => resolve(sourceDirectory, reference.root) === entry)
160
161
  const outputFile = resolve(root, output)
161
- const url = assetPath(base, relative(outputDirectory, outputFile).replaceAll(sep, "/"))
162
+ const url = assetPath(base, relative(resolve(assetsDirectory, ".."), outputFile).replaceAll(sep, "/"))
162
163
  for (const reference of rootReferences) emitted.set(reference.placeholder, url)
163
164
  }
164
165
  for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${reference.root}`)
@@ -167,3 +168,9 @@ export function createWorkerCompiler({
167
168
 
168
169
  return { candidate, emit, rejectConstructions, rejectOrdinaryImports, rewriteEffect }
169
170
  }
171
+
172
+ export function emitWorkers(references, sourceFiles, assetsDirectory, base, minify) {
173
+ const root = process.cwd()
174
+ const sourceDirectory = resolve(root, "src")
175
+ return createWorkerCompiler({ root, sourceDirectory, assetPath, parseSourceFile, resolveSourceImport, runtimeModuleReference }).emit(references, sourceFiles, assetsDirectory, base, minify)
176
+ }
@@ -131,6 +131,46 @@ export type ListDescriptor = {
131
131
  valueSeed?: Record<string, string | number | boolean | null>
132
132
  }
133
133
 
134
+ export interface RouteIR {
135
+ version: 1
136
+ states: Array<{ slot: number; id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route"; internal?: true }>
137
+ params: Array<{ name: string; id: string }>
138
+ searchParams: Array<{ name: string; id: string }>
139
+ searchParamsWritable: boolean
140
+ events: Array<{
141
+ event: string
142
+ commands?: Array<[string, string, unknown]>
143
+ native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
144
+ }>
145
+ effects: Array<{
146
+ module: string
147
+ handler: string
148
+ states: Record<string, string>
149
+ scope: Record<string, unknown>
150
+ lifetime?: "layout" | "route"
151
+ dependencies?: string[]
152
+ dependencyExpressions?: unknown[]
153
+ dependencyStates?: Record<string, string>
154
+ itemDependencies?: string[]
155
+ listState?: string
156
+ cleanup?: true
157
+ owner?: string
158
+ list?: true
159
+ }>
160
+ bindings: Array<{
161
+ target: string
162
+ state?: string
163
+ module?: string
164
+ handler?: string
165
+ states?: Record<string, string>
166
+ scope?: Record<string, unknown>
167
+ scopeStates?: Record<string, string>
168
+ scopeBindings?: Record<string, unknown>
169
+ }>
170
+ conditions: Array<Record<string, unknown>>
171
+ lists: ListDescriptor[]
172
+ }
173
+
134
174
  export function renderPage<Props = Record<string, never>>(
135
175
  component: (props: Props) => unknown | Promise<unknown>,
136
176
  metadata?: PageMetadata,
@@ -146,42 +186,5 @@ export function renderPage<Props = Record<string, never>>(
146
186
  hasListStyles: boolean
147
187
  hasStateSeed: boolean
148
188
  handlerModules: string[]
149
- plan: {
150
- states: Array<{ id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route"; internal?: true }>
151
- params: Array<{ name: string; id: string }>
152
- searchParams: Array<{ name: string; id: string }>
153
- searchParamsWritable: boolean
154
- events: Array<{
155
- event: string
156
- commands?: Array<[string, string, unknown]>
157
- native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
158
- }>
159
- effects: Array<{
160
- module: string
161
- handler: string
162
- states: Record<string, string>
163
- scope: Record<string, unknown>
164
- lifetime?: "layout" | "route"
165
- dependencies?: string[]
166
- dependencyExpressions?: unknown[]
167
- dependencyStates?: Record<string, string>
168
- itemDependencies?: string[]
169
- listState?: string
170
- cleanup?: true
171
- owner?: string
172
- list?: true
173
- }>
174
- bindings: Array<{
175
- target: string
176
- state?: string
177
- module?: string
178
- handler?: string
179
- states?: Record<string, string>
180
- scope?: Record<string, unknown>
181
- scopeStates?: Record<string, string>
182
- scopeBindings?: Record<string, unknown>
183
- }>
184
- conditions: Array<Record<string, unknown>>
185
- lists: ListDescriptor[]
186
- }
189
+ plan: RouteIR
187
190
  }>
@@ -527,7 +527,8 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
527
527
  hasStateSeed: initialState.length > 0,
528
528
  handlerModules: [...renderContext.handlerModules],
529
529
  plan: {
530
- states: Object.entries(renderContext.states).map(([id, state]) => ({ id, ...state })),
530
+ version: 1,
531
+ states: Object.entries(renderContext.states).map(([id, state], slot) => ({ slot, id, ...state })),
531
532
  params: renderContext.paramEntries,
532
533
  searchParams: renderContext.searchParamEntries,
533
534
  searchParamsWritable: renderContext.searchParamsWritable,
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto"
2
2
  import { readFile, stat, watch } from "node:fs/promises"
3
3
  import { createServer } from "node:http"
4
4
  import { extname, join, resolve, sep } from "node:path"
5
+ import { browserPath, withBase } from "./compiler/path-helpers.mjs"
5
6
  import { stateSchema } from "./dev-state.js"
6
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>`
@@ -216,14 +217,6 @@ function escapeHtml(value) {
216
217
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
217
218
  }
218
219
 
219
- function browserPath(path) {
220
- return path ? new URL(path, "http://kudzu.local").pathname : ""
221
- }
222
-
223
- function withBase(base, path) {
224
- return base ? `${base}${path}` : path
225
- }
226
-
227
220
  async function exists(path) {
228
221
  try {
229
222
  await stat(path)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.21",
3
+ "version": "0.8.23",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",