@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.
@@ -1,41 +1,38 @@
1
1
  import { createHash, randomUUID } from "node:crypto"
2
- import { cp, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises"
3
- import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path"
2
+ import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"
3
+ import { dirname, join, relative, resolve, sep } from "node:path"
4
4
  import { pathToFileURL } from "node:url"
5
5
  import { build as bundle, transform } from "esbuild"
6
- import ts from "typescript"
7
- import { createComponentAnalysisSession } from "./compiler/analysis/component-analysis.mjs"
8
- import { normalizeEffectAnimationFrameRefs } from "./compiler/animation-frame-pass.mjs"
9
- import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./compiler/ast-helpers.mjs"
10
- import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditions } from "./compiler/browser-signal-passes.mjs"
11
- import { analyzeCollectionPipeline, collectionExpression, collectionParameters, isArrayFromCall, mutatingCollectionMethods as mutatingListMethods, pureCollectionMathMethods as pureMathMethods, pureCollectionMethods as pureListMethods } from "./compiler/collection-analysis.mjs"
12
- import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.mjs"
13
- import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
14
- import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "./compiler/effect-analysis.mjs"
15
6
  import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
16
- import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
17
- import { createHandlerLowering } from "./compiler/handler-lowering.mjs"
18
- import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
19
- import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
20
- import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
21
- import { normalizeRenderControlFlow } from "./compiler/render-control-pass.mjs"
22
- import { createRouterPass } from "./compiler/router-pass.mjs"
7
+ import { generateListRuntime } from "./compiler/list-runtime-codegen.mjs"
8
+ import { assetPath, browserPath, relativeModulePath, withBase } from "./compiler/path-helpers.mjs"
9
+ import { clientModulePath, collectClientModules, compileClientModule, compiledPath, compileSource, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles } from "./compiler/source-compiler.mjs"
10
+ import { createParamCodegen } from "./compiler/param-codegen.mjs"
23
11
  import { planRouteCapabilities, usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
24
- import { createWorkerCompiler } from "./compiler/worker-compiler.mjs"
25
- import { createZustandPass } from "./compiler/zustand-pass.mjs"
12
+ import { generateBindingRuntime, generateCoreRuntime, generateEffectRuntime, generateNativeRuntime, generateNavigationRuntime, specializeRuntime } from "./compiler/runtime-codegen.mjs"
13
+ import { emitWorkers } from "./compiler/worker-compiler.mjs"
26
14
  import { renderPage } from "./core.mjs"
27
15
  import { parseDevHost, parseDevPort, startDevServer } from "./dev-server.mjs"
28
16
 
29
17
  export { parseDevHost, parseDevPort }
18
+ export { specializeRuntime }
30
19
 
31
20
  const root = process.cwd()
32
21
  const sourceDirectory = join(root, "src")
33
22
  const pagesDirectory = join(sourceDirectory, "pages")
34
23
  const workDirectory = join(root, ".kudzu")
35
24
  const outputDirectory = join(root, "dist")
36
- const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
37
- const compileEventCommand = createCommandSpecializer({ isPrimitiveLiteral: isPrimitiveDefaultLiteral })
38
- const { analyzeZustandStores, normalizeZustandMigrationSyntax } = createZustandPass({ isSerializableStateLiteral, nativeCaptureNames, sourceDirectory })
25
+
26
+ async function loadConfig() {
27
+ for (const name of ["kudzu.config.mjs", "kudzu.config.js"]) {
28
+ const file = join(root, name)
29
+ if (!(await exists(file))) continue
30
+ const config = (await import(`${pathToFileURL(file).href}?v=${Date.now()}-${randomUUID()}`)).default ?? {}
31
+ if (!isPlainRecord(config)) throw new Error(`${name} must export a default object`)
32
+ return config
33
+ }
34
+ return {}
35
+ }
39
36
 
40
37
  export async function build({ quiet = false, minify = true } = {}) {
41
38
  const config = await loadConfig()
@@ -78,7 +75,12 @@ export async function build({ quiet = false, minify = true } = {}) {
78
75
  const sourceResults = []
79
76
  for (const file of sourceFiles) {
80
77
  if (file.endsWith(".worker.ts")) continue
81
- sourceResults.push(await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base))
78
+ const result = compileSource(file, sourceFileSet, sourceIndex, staticFiles, cssModules, base)
79
+ for (const asset of result.importedAssets) importedAssets.add(resolve(root, asset))
80
+ const output = resolve(root, result.buildModule.path)
81
+ await mkdir(dirname(output), { recursive: true })
82
+ await writeFile(output, result.buildModule.code)
83
+ sourceResults.push(result)
82
84
  }
83
85
  const handlerModules = sourceResults.flatMap(result => result.handlerModule ? [result.handlerModule] : [])
84
86
  const workerReferences = sourceResults.flatMap(result => result.moduleIR.effects.flatMap(effect => {
@@ -204,7 +206,7 @@ export async function build({ quiet = false, minify = true } = {}) {
204
206
  const renderedEffects = new Set(plans.flatMap(plan => plan.effects.map(effect => `${effect.module}:${effect.handler}`)))
205
207
  const renderedWorkerReferences = workerReferences.filter(reference => renderedEffects.has(`${reference.module}:${reference.handler}`))
206
208
  if (renderedWorkerReferences.length && await exists(join(publicDirectory, "assets", "workers"))) throw new Error("public/assets/workers collides with Kudzu's generated Worker asset namespace")
207
- const workerAssets = await workerCompiler.emit(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
209
+ const workerAssets = await emitWorkers(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
208
210
  for (const module of emittedHandlerModules) {
209
211
  for (const reference of workerReferences) {
210
212
  if (reference.module !== assetPath(base, `assets/${module.path}`)) continue
@@ -213,41 +215,16 @@ export async function build({ quiet = false, minify = true } = {}) {
213
215
  }
214
216
  if (module.code.includes("/__kudzu_worker_")) throw new Error(`Worker URL placeholder survived in ${module.path}`)
215
217
  }
216
- const capabilityManifest = planRouteCapabilities(plans, { routes: routeCapabilities, navigationRouteCount: navigationRoutes.length })
218
+ const capabilityIR = planRouteCapabilities(plans, { routes: routeCapabilities, navigationRouteCount: navigationRoutes.length })
217
219
  const {
218
- routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount, regularStateSeeds: regularStateSeedCount, dependencyStateSeeds: dependencyStateSeedCount },
219
- events: { command: commandEvents, native: nativeEvents, hasNativeHandlers },
220
- bindings: { count: bindingCount, text: hasTextBindings, svgConditions: hasSvgConditions },
221
- lists: {
222
- count: listCount,
223
- styleCount: listStyleCount,
224
- conditions: hasListConditions,
225
- svg: hasSvgLists,
226
- deepConditions: hasDeepListConditions,
227
- textRanges: hasListTextRanges,
228
- attributes: hasListAttributes,
229
- events: hasListEvents,
230
- expressions: hasListExpressions,
231
- expressionAttributes: hasListExpressionAttributes,
232
- seeds: hasListSeeds,
233
- effects: hasListEffects,
234
- rowHooks: hasListRowHooks,
235
- rowRefs: hasListRowRefs,
236
- complexRowState: hasComplexListRowState,
237
- nested: hasNestedLists,
238
- selectors: hasCollectionSelectors,
239
- calculated: hasCalculatedCollections,
240
- static: hasStaticCollections,
241
- indexes: hasListIndexes,
242
- stableFastPaths: hasListStableFastPaths,
243
- generalRowHooks: hasGeneralListRowHooks,
244
- asyncParts: hasListAsyncParts,
245
- mounts: hasListMounts
246
- },
247
- effects: { any: hasEffects, derivedDependencies: hasDerivedEffectDependencies, itemDependencies: hasItemDependencies, captures: hasEffectCaptures, navigable: hasNavigableEffects, navigableOwners: hasNavigableOwners },
220
+ routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount, dependencyStateSeeds: dependencyStateSeedCount },
221
+ events: { command: commandEvents, hasNativeHandlers },
222
+ bindings: { count: bindingCount },
223
+ lists,
224
+ effects: { any: hasEffects, derivedDependencies: hasDerivedEffectDependencies, captures: hasEffectCaptures },
248
225
  captures: { nestedState: hasNestedStateCaptures, setter: hasSetterCaptures },
249
226
  runtime: { shared: hasSharedRuntime, dependency: hasDependencyRuntime }
250
- } = capabilityManifest
227
+ } = capabilityIR
251
228
  const runtimeName = usesDependencyRuntime => usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
252
229
  for (const entry of pageEntries) {
253
230
  const routeDirectory = join(outputDirectory, entry.route)
@@ -257,12 +234,7 @@ export async function build({ quiet = false, minify = true } = {}) {
257
234
  }
258
235
  if (navigationRoutes.length || behaviorCount && (hasSharedRuntime || regularBehaviorCount)) {
259
236
  const runtimeFile = hasSharedRuntime ? "./shared-runtime.js" : "./runtime.js"
260
- let runtime = specializeRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), commandEvents, regularStateSeedCount > 0)
261
- if (!hasItemDependencies) runtime = runtime.replace(/\/\* list-item-hooks \*\/[\s\S]*?\/\* list-item-hooks-end \*\/\n/, "")
262
- if (hasNavigableEffects) runtime = runtime.replace("export function registerCommitter(commit) {\n committers.push(commit)\n}", "export function registerCommitter(commit) {\n committers.push(commit)\n return () => {\n const index = committers.indexOf(commit)\n if (index !== -1) committers.splice(index, 1)\n }\n}")
263
- if (hasNavigableOwners) runtime = runtime
264
- .replace("export function registerMountHook(mount) {\n mountHooks.push(mount)\n}", "export function registerMountHook(mount) {\n mountHooks.push(mount)\n return () => {\n const index = mountHooks.indexOf(mount)\n if (index !== -1) mountHooks.splice(index, 1)\n }\n}")
265
- .replace("export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n}", "export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n return () => {\n const index = unmountHooks.indexOf(unmount)\n if (index !== -1) unmountHooks.splice(index, 1)\n }\n}")
237
+ const runtime = generateCoreRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), capabilityIR)
266
238
  await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
267
239
  }
268
240
  if (hasDependencyRuntime) {
@@ -274,129 +246,30 @@ export async function build({ quiet = false, minify = true } = {}) {
274
246
  "globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
275
247
  })
276
248
  if (hasEffects) {
277
- let effectRuntime = await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8")
278
- effectRuntime = hasEffectCaptures ? effectRuntime.replace('"./serialization.js"', '"./kudzu-serialization.js"') : effectRuntime.replace(/^import[^\n]+\n/, "")
279
- await writeBundledJavaScript(join(assetsDirectory, "kudzu-effect.js"), effectRuntime, minify, {
280
- "globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures),
281
- "globalThis.__KUDZU_EFFECT_CAPTURES__": String(hasEffectCaptures)
282
- })
249
+ const generated = generateEffectRuntime(await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8"), capabilityIR)
250
+ await writeBundledJavaScript(join(assetsDirectory, "kudzu-effect.js"), generated.source, minify, generated.define)
283
251
  }
284
- if (bindingCount || listStyleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
252
+ if (bindingCount || lists.styleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
285
253
  if (bindingCount) {
286
- let bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
287
- .replace('"./shared-runtime.js"', '"./kudzu.js"')
288
- .replace('"./serialization.js"', '"./kudzu-serialization.js"')
289
- .replace('"./style.js"', '"./kudzu-style.js"')
290
- if (navigationRoutes.length) bindingRuntime = specializeNavigationTextDescriptors(bindingRuntime)
291
- await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify, {
292
- "globalThis.__KUDZU_TEXT_BINDINGS__": String(hasTextBindings),
293
- "globalThis.__KUDZU_SVG_CONDITIONS__": String(hasSvgConditions),
294
- "globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures)
295
- })
254
+ const generated = generateBindingRuntime(await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"), capabilityIR, navigationRoutes.length > 0)
255
+ await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), generated.source, minify, generated.define)
296
256
  }
297
257
  if (hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
298
- if (listCount) {
299
- if (hasCollectionSelectors && !hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
300
- let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
301
- .replace('"./shared-runtime.js"', '"./kudzu.js"')
302
- listRuntime = hasCalculatedCollections
303
- ? listRuntime.replace('"./binding-runtime.js"', '"./kudzu-binding.js"')
304
- : listRuntime.replace(/^const loadListEvaluator[^\n]+\n/m, "")
305
- listRuntime = hasCollectionSelectors
306
- ? listRuntime.replace('"./collection-selector.js"', '"./kudzu-collection-selector.js"')
307
- : listRuntime.replace(/^import \{ selectCollection \}[^\n]+\n/m, "")
308
- if (!hasListIndexes) listRuntime = listRuntime
309
- .replace("for (const [index, item] of items.entries()) {", "for (const item of items) {")
310
- .replace("const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]", "const key = item?.[list.descriptor.key]")
311
- .replace("entries.push({ item, index, key, token, value:", "entries.push({ item, key, token, value:")
312
- .replace("for (const { item, index, key, token, value } of entries) {", "for (const { item, key, token, value } of entries) {")
313
- .replaceAll("fillListItem(node, item, list.descriptor.nested, index)", "fillListItem(node, item, list.descriptor.nested)")
314
- .replace("fillListItem(node, item, list.descriptor.nested, index, mapListItemParts", "fillListItem(node, item, list.descriptor.nested, 0, mapListItemParts")
315
- .replace("function addListRoot(list, { item, index = list.roots.size, key, token, value })", "function addListRoot(list, { item, key, token, value })")
316
- .replace("fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)", "fillListParts(root, listItemParts(root), listItems.get(owner), 0)")
317
- .replace("function fillListItem(root, item, nested = false, index = 0)", "function fillListItem(root, item, nested = false)")
318
- .replace("fillListParts(root, parts, item, revision, index, previous)", "fillListParts(root, parts, item, revision, previous)")
319
- .replace("function fillListParts(root, parts, item, revision, index = 0, previous)", "function fillListParts(root, parts, item, revision, previous)")
320
- .replace("fillListExpressions(root, parts, item, revision, index)", "fillListExpressions(root, parts, item, revision)")
321
- .replaceAll('value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value', 'value?.type === "list-item" ? serializeItem(item) : value')
322
- .replaceAll("evaluate(descriptor, item, index)", "evaluate(descriptor, item)")
323
- .replaceAll("evaluate({ module, handler }, item, index)", "evaluate({ module, handler }, item)")
324
- .replace("updateListCondition(marker, descriptor.kind, value, item, index)", "updateListCondition(marker, descriptor.kind, value, item)")
325
- .replace("function updateListCondition(marker, kind, value, item, index)", "function updateListCondition(marker, kind, value, item)")
326
- .replace("fillListParts(marker, listItemParts(fragment), item, revision, index)", "fillListParts(marker, listItemParts(fragment), item, revision)")
327
- .replace("function evaluate(descriptor, item, index)", "function evaluate(descriptor, item)")
328
- .replace("exports[descriptor.handler](item, index)", "exports[descriptor.handler](item)")
329
- .replace("exports[descriptor.handler](item, index, {", "exports[descriptor.handler](item, undefined, {")
330
- if (!hasCollectionSelectors) listRuntime = listRuntime.replaceAll(" && !list.descriptor.selector", "")
331
- if (!hasListIndexes) listRuntime = listRuntime
332
- .replaceAll(" && !list.descriptor.indexed", "")
333
- .replaceAll(" && list.descriptor.key !== null", "")
334
- .replaceAll("list.descriptor.key !== null && !list.descriptor.indexed && ", "")
335
- .replace("list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && list.values.size", "list.values.size")
336
- .replace("(referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value) || list.descriptor.indexed || list.descriptor.key === null", "referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value")
337
- if (hasListRowHooks && !hasGeneralListRowHooks) listRuntime = listRuntime
338
- .replace(/\/\* general-row-hooks \*\/[\s\S]*?\/\* general-row-hooks-end \*\/\n/, "")
339
- .replaceAll("initializeGeneralRowHooks", "initializeRowStates")
340
- .replace("if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index], roots[index], nested?.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])")
341
- .replaceAll("if (__KUDZU_LIST_ROW_HOOKS__) initializeRowStates(list.descriptor, key, node, list.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)")
342
- .replace("for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))", "for (const token of registration.list.roots.keys()) deleteFlatRowStates(registration.list.descriptor, token)")
343
- .replaceAll("deleteRowStates(list.descriptor, ownershipPaths.get(node))", "deleteFlatRowStates(list.descriptor, token)")
344
- .replace(" if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))\n", "")
345
- .replace(" if (!replacements) return\n", "")
346
- if (!hasItemDependencies) listRuntime = listRuntime.replace(", notifyListItem", "")
347
- if (!hasListStableFastPaths) listRuntime = listRuntime.replace(/\/\* stable-list-fast-path \*\/[\s\S]*?\/\* stable-list-fast-path-end \*\/\n/, "")
348
- const stylePatch = ` if (target === "style") {
349
- const style = serializeStyle(value)
350
- if (style) node.setAttribute("style", style)
351
- else node.removeAttribute("style")
352
- return
353
- }`
354
- listRuntime = listRuntime.replace(" /* list-style */", listStyleCount ? stylePatch : "")
355
- if (listStyleCount) listRuntime = `import { serializeStyle } from "./kudzu-style.js"\n${listRuntime}`
356
- await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), listRuntime, minify, {
357
- __KUDZU_LIST_CONDITIONS__: String(hasListConditions),
358
- __KUDZU_DEEP_LIST_CONDITIONS__: String(hasDeepListConditions),
359
- __KUDZU_LIST_TEXT_RANGES__: String(hasListTextRanges),
360
- __KUDZU_LIST_ATTRIBUTES__: String(hasListAttributes),
361
- __KUDZU_LIST_EVENTS__: String(hasListEvents),
362
- __KUDZU_LIST_EXPRESSIONS__: String(hasListExpressions),
363
- __KUDZU_LIST_EXPRESSION_ATTRIBUTES__: String(hasListExpressionAttributes),
364
- __KUDZU_LIST_SEEDS__: String(hasListSeeds),
365
- __KUDZU_LIST_EFFECTS__: String(hasListEffects),
366
- __KUDZU_LIST_ASYNC_PARTS__: String(hasListAsyncParts),
367
- __KUDZU_LIST_MOUNTS__: String(hasListMounts),
368
- __KUDZU_LIST_ITEM_HOOKS__: String(hasItemDependencies),
369
- __KUDZU_LIST_ROW_HOOKS__: String(hasListRowHooks),
370
- __KUDZU_LIST_ROW_REFS__: String(hasListRowRefs),
371
- __KUDZU_COMPLEX_LIST_ROW_STATE__: String(hasComplexListRowState),
372
- __KUDZU_NESTED_LISTS__: String(hasNestedLists),
373
- __KUDZU_COLLECTION_SELECTORS__: String(hasCollectionSelectors),
374
- __KUDZU_STATIC_COLLECTIONS__: String(hasStaticCollections),
375
- __KUDZU_LIST_INDEXES__: String(hasListIndexes),
376
- __KUDZU_LIST_STABLE_FAST_PATHS__: String(hasListStableFastPaths),
377
- __KUDZU_SVG_LISTS__: String(hasSvgLists)
378
- })
379
- if (hasCollectionSelectors && !hasDerivedEffectDependencies) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
258
+ if (lists.count) {
259
+ if (lists.selectors && !hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
260
+ const generated = generateListRuntime(await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"), capabilityIR)
261
+ await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), generated.source, minify, generated.define)
262
+ if (lists.selectors && !hasDerivedEffectDependencies) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
380
263
  }
381
264
  if (hasNativeHandlers) {
382
- const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
383
- .replace('"./shared-runtime.js"', '"./kudzu.js"')
384
- .replace('"./serialization.js"', '"./kudzu-serialization.js"')
385
- await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeEvents(nativeRuntime, nativeEvents), minify, {
386
- "globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
387
- })
265
+ const generated = generateNativeRuntime(await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"), capabilityIR)
266
+ await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), generated.source, minify, generated.define)
388
267
  for (const entry of nativeEntries) await printNativeEntry(entry, assetsDirectory, base, minify)
389
268
  }
390
269
  if (navigationGroups.length) {
391
270
  const navigationSource = await readFile(new URL("./navigation-runtime.js", import.meta.url), "utf8")
392
271
  for (const group of navigationGroups) {
393
- let navigationRuntime = navigationSource
394
- .replace("__KUDZU_NAVIGATION_ROUTES__", inlineJson(group.records))
395
- .replace("__KUDZU_APPLICATION_ID__", JSON.stringify(group.applicationId))
396
- .replace("__KUDZU_LAYOUT_ID__", JSON.stringify(group.layoutId))
397
- .replace('"./shared-runtime.js"', '"./kudzu.js"')
398
- navigationRuntime = specializeNavigationPatterns(navigationRuntime, group.records.some(record => record.segments))
399
- await writeJavaScript(join(assetsDirectory, group.assetName), specializeNavigationEffects(navigationRuntime, group.hasEffects || group.hasParams), minify)
272
+ await writeJavaScript(join(assetsDirectory, group.assetName), generateNavigationRuntime(navigationSource, group), minify)
400
273
  }
401
274
  }
402
275
  for (const handlerModule of emittedHandlerModules) {
@@ -414,11 +287,13 @@ export async function build({ quiet = false, minify = true } = {}) {
414
287
  await mkdir(dirname(output), { recursive: true })
415
288
  await writeJavaScript(output, printEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base, entry.paramPath, runtimeName(entry.usesDependencyRuntime), entry.navigable), minify)
416
289
  }
417
- const clientModules = await collectClientModules(emittedHandlerModules.flatMap(module => module.clientImports), sourceFileSet)
290
+ const clientModules = await collectClientModules(emittedHandlerModules.flatMap(module => module.clientImports).map(file => resolve(root, file)), sourceFileSet)
418
291
  for (const file of clientModules) {
419
- const output = join(assetsDirectory, clientModulePath(file))
420
- await mkdir(resolve(output, ".."), { recursive: true })
421
- await writeJavaScript(output, await compileClientModule(file, sourceFileSet, staticFiles, importedAssets, cssModules, base), minify)
292
+ const module = await compileClientModule(file, sourceFileSet, staticFiles, cssModules, base)
293
+ for (const asset of module.importedAssets) importedAssets.add(resolve(root, asset))
294
+ const output = join(assetsDirectory, module.path)
295
+ await mkdir(dirname(output), { recursive: true })
296
+ await writeJavaScript(output, module.code, minify)
422
297
  }
423
298
  if (clientModules.length || emittedHandlerModules.some(module => module.hasPackageImports)) {
424
299
  await bundle({
@@ -483,64 +358,6 @@ function preloadModules(html) {
483
358
  return html.replace(scripts[0][0], `${links}${scripts[0][0]}`)
484
359
  }
485
360
 
486
- function specializeEvents(source, events) {
487
- return source.replace(/const eventNames = \[[^\n]+\]/, `const eventNames = ${JSON.stringify(events)}`)
488
- }
489
-
490
- function specializeNavigationEffects(source, enabled) {
491
- if (enabled) return source
492
- return source
493
- .replace("const noDispose = async () => {}\nlet routeDispose = noDispose\nlet layoutDispose = noDispose\nconst ready = mountInitial()\n", "")
494
- .replace(`addEventListener("pagehide", event => {
495
- if (event.persisted) return
496
- ++revision
497
- request?.abort()
498
- void (async () => {
499
- await routeDispose()
500
- await layoutDispose()
501
- })()
502
- })
503
- `, "")
504
- .replace(`
505
- async function mountInitial() {
506
- try {
507
- const record = matchRoute(location.pathname)
508
- if (!record) throw new Error("Initial navigation route does not match")
509
- const capabilities = await loadCapabilities(validate(document, record))
510
- capabilities.params?.(location.pathname, location.search)
511
- layoutDispose = await capabilities.effects?.mountLayoutEffects?.() ?? noDispose
512
- routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose
513
- } catch (error) {
514
- console.error(error)
515
- }
516
- }
517
- `, "")
518
- .replace(" await ready\n", "")
519
- .replace(" const { incoming, parsed, capabilities } = documentResult\n", " const { incoming, parsed } = documentResult\n")
520
- .replace(" await routeDispose()\n if (current !== revision) return\n", "")
521
- .replace(" commit(incoming, parsed.nodes, capabilities.params, url.pathname, url.search)\n", " commit(incoming, parsed.nodes)\n")
522
- .replace(" routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose\n", "")
523
- .replace(" return { incoming, parsed, capabilities: await loadCapabilities(parsed), record }\n", " await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))\n return { incoming, parsed, record }\n")
524
- .replace(`
525
- async function loadCapabilities(parsed) {
526
- const modules = await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))
527
- const params = modules.filter(module => typeof module.initializeParams === "function")
528
- const effects = modules.filter(module => typeof module.mountRouteEffects === "function")
529
- if (params.length > 1 || effects.length > 1) throw new Error("Navigation document has duplicate route capabilities")
530
- return { params: params[0]?.initializeParams, effects: effects[0] }
531
- }
532
- `, "")
533
- }
534
-
535
- function specializeNavigationPatterns(source, enabled) {
536
- if (enabled) return source
537
- return source.replace(/function matchRoute\(pathname\) \{[\s\S]+?\n\}\n\nfunction fallback/, `function matchRoute(pathname) {
538
- return routes.find(record => record.path === pathname)
539
- }
540
-
541
- function fallback`)
542
- }
543
-
544
361
  async function printNativeEntry(entry, assetsDirectory, base, minify) {
545
362
  const output = join(assetsDirectory, entry.path)
546
363
  await mkdir(dirname(output), { recursive: true })
@@ -566,83 +383,6 @@ function runtimeEffects(effects, lifetimes = false) {
566
383
  }))
567
384
  }
568
385
 
569
- function printParamEntry(schema, params, searchParams, searchParamsWritable, output, assetsDirectory, base, runtimeName, navigable) {
570
- const hasSearch = searchParams.length || searchParamsWritable
571
- const signature = hasSearch ? "pathname, search" : "pathname"
572
- 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" : ""}`
573
- const suffix = navigable ? "\n}" : ""
574
- const pathname = schema ? `const base = ${inlineJson(browserPath(base).slice(1).split("/").filter(Boolean).map(segment => decodeURIComponent(segment)))}
575
- const schema = ${inlineJson(schema.segments)}
576
- const params = ${inlineJson(params)}
577
- let path = pathname
578
- if (base.length) {
579
- const pathSegments = path.slice(1).split("/")
580
- if (pathSegments.length < base.length || base.some((segment, index) => decodeSegment(pathSegments[index], false) !== segment)) throw new Error("Runtime route is outside the configured base")
581
- path = "/" + pathSegments.slice(base.length).join("/")
582
- }
583
- if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1)
584
- const segments = path.slice(1).split("/")
585
- if (segments.length !== schema.length) throw new Error("Runtime route does not match its fallback pattern")
586
- const values = Object.create(null)
587
- for (let index = 0; index < schema.length; index++) {
588
- const segment = schema[index]
589
- const value = decodeSegment(segments[index], Boolean(segment.param))
590
- if (segment.literal !== undefined && value !== segment.literal) throw new Error("Runtime route literal does not match")
591
- if (segment.param) values[segment.param] = value
592
- }
593
- for (const param of params) {
594
- const value = values[param.name]
595
- browserState.set(param.id, value)
596
- commitDom(param.id, value)
597
- }
598
- function decodeSegment(raw, param) {
599
- if (param && /%(?:2f|5c)/i.test(raw)) throw new Error("Runtime route parameter contains an encoded separator")
600
- let value
601
- try { value = decodeURIComponent(raw) } catch { throw new Error("Runtime route parameter has malformed encoding") }
602
- const decodedDots = value.replace(/%2e/gi, ".")
603
- if (param && (!value || value === "." || value === ".." || decodedDots === "." || decodedDots === ".." || /[\\/?#]/.test(value) || [...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159) || /%(?:2f|5c)/i.test(value))) throw new Error("Runtime route parameter is invalid")
604
- return value
605
- }
606
- ` : ""
607
- const searchInitializer = searchParamsWritable && searchParams.length ? `function initializeSearch(search) {
608
- const query = new URLSearchParams(search)
609
- for (const param of ${inlineJson(searchParams)}) {
610
- const value = query.get(param.name)
611
- browserState.set(param.id, value)
612
- commitDom(param.id, value)
613
- }
614
- }
615
- ` : ""
616
- const query = searchParams.length ? searchParamsWritable ? "initializeSearch(search)\n" : `const query = new URLSearchParams(search)
617
- for (const param of ${inlineJson(searchParams)}) {
618
- const value = query.get(param.name)
619
- browserState.set(param.id, value)
620
- commitDom(param.id, value)
621
- }
622
- ` : ""
623
- const writer = searchParamsWritable ? `
624
- function setSearchParams(update, replace) {
625
- const next = update(new URLSearchParams(location.search))
626
- if (!(next instanceof URLSearchParams)) throw new Error("React Router search parameter updater must return URLSearchParams")
627
- const url = new URL(location.href)
628
- url.search = next.toString()
629
- history[replace ? "replaceState" : "pushState"](null, "", url)
630
- ${searchParams.length ? "initializeSearch(location.search)" : ""}
631
- }
632
- ${navigable ? "" : `globalThis.__kSetSearchParams = setSearchParams
633
- addEventListener("popstate", () => ${searchParams.length ? "initializeSearch(location.search)" : "undefined"})`}` : ""
634
- return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}
635
- ${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}`
636
- }
637
-
638
- export function specializeRuntime(source, events, hasStateSeed) {
639
- const specialized = specializeEvents(source, events)
640
- if (hasStateSeed) return specialized
641
- return specialized
642
- .replace(" const initialState = document.body.dataset.kState\n", "")
643
- .replace(/^ if \(initialState\).*\n/m, "")
644
- }
645
-
646
386
  async function writeJavaScript(file, source, minify, define) {
647
387
  const code = minify || define ? (await transform(source, { define, format: "esm", legalComments: "none", minify, target: "es2022" })).code : source
648
388
  await writeFile(file, code)
@@ -683,3173 +423,210 @@ function escapeAttribute(value) {
683
423
  return escapeHtml(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;")
684
424
  }
685
425
 
686
- async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base) {
687
- const source = sourceIndex.get(file)
688
- const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
689
- const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
690
- const result = ts.transpileModule(source, {
691
- fileName: file,
692
- compilerOptions: {
693
- target: ts.ScriptTarget.ES2022,
694
- module: ts.ModuleKind.ESNext,
695
- jsx: ts.JsxEmit.ReactJSX,
696
- jsxImportSource: "@kudzujs/core"
697
- },
698
- transformers: { before: [createKudzuTransformer({ semantic, handlerUrl: assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base })] },
699
- reportDiagnostics: true
700
- })
701
-
702
- const errors = result.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
703
- if (errors.length) {
704
- throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
705
- }
706
- const packageReference = emittedPackageReference(result.outputText, file, new Set(["react", "react-router-dom"]))
707
- if (packageReference) throw new Error(`${relative(root, file)} Runtime ${packageReference} module references are not supported`)
708
-
709
- const output = compiledPath(file)
710
- await mkdir(resolve(output, ".."), { recursive: true })
711
- await writeFile(output, result.outputText)
712
-
713
- const { componentAnalysis, moduleIR } = semantic
714
- const sourceResult = { file: relative(root, file).replaceAll(sep, "/"), componentAnalysis, moduleIR }
715
- const moduleHandlers = moduleIR.handlers.filter(handler => handler.kind === "module-export")
716
- if (!moduleHandlers.length && !moduleIR.bindings.length) return sourceResult
717
- const moduleSource = printHandlerModule({ moduleIR, handlerPath })
718
- const moduleResult = ts.transpileModule(moduleSource, {
719
- compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
720
- reportDiagnostics: true
721
- })
722
- const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
723
- if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
724
- sourceResult.handlerModule = { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: moduleHandlers.some(handler => handler.role === "native"), hasEffects: moduleHandlers.some(handler => handler.role === "effect"), clientImports: moduleIR.clientModules, hasPackageImports: moduleIR.imports.some(entry => entry.package) }
725
- return sourceResult
726
- }
727
-
728
- function emittedPackageReference(source, file, packages) {
729
- const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS)
730
- let found
731
- const visit = node => {
732
- if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && packages.has(node.moduleSpecifier.text)) found = node.moduleSpecifier.text
733
- if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && packages.has(node.arguments[0].text)) found = node.arguments[0].text
734
- if (!found) ts.forEachChild(node, visit)
426
+ async function prepareSourceStyles(cssFiles, staticFiles, importedAssets, base) {
427
+ const cssModules = new Map()
428
+ const cssOutputs = new Map()
429
+ for (const file of cssFiles) {
430
+ let css = rewriteCssUrls(await readFile(file, "utf8"), file, staticFiles, importedAssets, base)
431
+ if (file.toLowerCase().endsWith(".module.css")) {
432
+ if (/\bcomposes\s*:/i.test(maskCssCommentsAndStrings(css))) throw new Error(`${relative(root, file)} CSS Modules composes is not supported`)
433
+ const prefix = `k${createHash("sha256").update(relative(sourceDirectory, file).replaceAll(sep, "/")).digest("hex").slice(0, 8)}`
434
+ css = (await transform(css, { loader: "local-css", sourcefile: `${prefix}.css`, target: "es2022" })).code
435
+ const classes = {}
436
+ for (const match of css.matchAll(new RegExp(`\\.${prefix}_([_a-zA-Z][_a-zA-Z0-9-]*)`, "g"))) classes[match[1]] = match[0].slice(1)
437
+ cssModules.set(file, classes)
438
+ }
439
+ cssOutputs.set(file, css)
735
440
  }
736
- visit(sourceFile)
737
- return found
441
+ return { cssModules, cssOutputs }
738
442
  }
739
443
 
740
- function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
741
- const reachable = new Set()
742
- const queue = [...entries]
743
- while (queue.length) {
744
- const file = queue.pop()
745
- if (reachable.has(file)) continue
746
- reachable.add(file)
747
- const sourceFile = parseSourceFile(file, sourceIndex.get(file))
748
- const visit = node => {
749
- const specifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && runtimeModuleReference(node) && node.moduleSpecifier
750
- if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
751
- try { queue.push(resolveSourceImport(file, specifier.text, sourceFiles)) } catch {}
752
- }
753
- const worker = workerCompiler.candidate(node, sourceFile)
754
- if (worker && ts.isStringLiteral(worker.url.arguments[0]) && worker.url.arguments[0].text.endsWith(".worker.ts")) {
755
- try { queue.push(resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles)) } catch {}
444
+ function rewriteCssUrls(css, file, staticFiles, importedAssets, base) {
445
+ let output = ""
446
+ let cursor = 0
447
+ let index = 0
448
+ while (index < css.length) {
449
+ if (css.startsWith("/*", index)) {
450
+ index = css.indexOf("*/", index + 2)
451
+ index = index === -1 ? css.length : index + 2
452
+ continue
453
+ }
454
+ if (css[index] === '"' || css[index] === "'") {
455
+ index = cssStringEnd(css, index)
456
+ continue
457
+ }
458
+ if (css.slice(index, index + 3).toLowerCase() !== "url" || /[-_a-z\d]/i.test(css[index - 1] ?? "")) {
459
+ index++
460
+ continue
461
+ }
462
+ let open = index + 3
463
+ while (/\s/.test(css[open] ?? "")) open++
464
+ if (css[open] !== "(") {
465
+ index++
466
+ continue
467
+ }
468
+ let start = open + 1
469
+ while (/\s/.test(css[start] ?? "")) start++
470
+ const quote = css[start] === '"' || css[start] === "'" ? css[start] : ""
471
+ const valueStart = quote ? start + 1 : start
472
+ let end = valueStart
473
+ if (quote) {
474
+ end = cssStringEnd(css, start) - 1
475
+ if (css[end] !== quote) {
476
+ index = open + 1
477
+ continue
756
478
  }
757
- ts.forEachChild(node, visit)
479
+ } else {
480
+ while (end < css.length && css[end] !== ")") end += css[end] === "\\" ? 2 : 1
481
+ }
482
+ let close = quote ? end + 1 : end
483
+ while (/\s/.test(css[close] ?? "")) close++
484
+ if (css[close] !== ")") {
485
+ index = open + 1
486
+ continue
758
487
  }
759
- visit(sourceFile)
488
+ const value = css.slice(valueStart, end).trim()
489
+ const replacement = rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base)
490
+ output += css.slice(cursor, index) + (replacement ?? css.slice(index, close + 1))
491
+ cursor = close + 1
492
+ index = close + 1
760
493
  }
761
- return [...reachable].sort()
494
+ return output + css.slice(cursor)
762
495
  }
763
496
 
764
- function normalizeClsxSyntax(sourceFile, factory, context) {
765
- const names = new Set()
766
- for (const statement of sourceFile.statements) {
767
- if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "clsx") continue
768
- if (statement.importClause?.name) names.add(statement.importClause.name.text)
769
- const bindings = statement.importClause?.namedBindings
770
- if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) if (!entry.isTypeOnly && (entry.propertyName ?? entry.name).text === "clsx") names.add(entry.name.text)
771
- }
772
- if (!names.size) return sourceFile
497
+ function rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base) {
498
+ if (!value || value.startsWith("/") || value.startsWith("#") || value.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(value)) return undefined
499
+ const split = value.search(/[?#]/)
500
+ const pathname = split === -1 ? value : value.slice(0, split)
501
+ const suffix = split === -1 ? "" : value.slice(split)
502
+ const target = resolve(dirname(file), pathname)
503
+ if (!staticFiles.has(target)) throw new Error(`${relative(root, file)} CSS URL ${JSON.stringify(value)} must resolve to an existing regular file under src/`)
504
+ importedAssets.add(target)
505
+ const url = assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`)
506
+ return `url(${quote || '"'}${url}${suffix}${quote || '"'})`
507
+ }
773
508
 
774
- const lower = node => {
775
- node = unwrapExpression(node)
776
- if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return node
777
- if (node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword) return factory.createStringLiteral("")
778
- if (ts.isConditionalExpression(node)) return factory.updateConditionalExpression(node, node.condition, node.questionToken, lower(node.whenTrue), node.colonToken, lower(node.whenFalse))
779
- if (ts.isArrayLiteralExpression(node)) return combine(node.elements.map(lower))
780
- if (ts.isObjectLiteralExpression(node)) return combine(node.properties.map(property => {
781
- if (!ts.isPropertyAssignment(property) || property.name && ts.isComputedPropertyName(property.name)) throw sourceNodeError(property, sourceFile, "clsx() object arguments require ordinary key/value properties")
782
- const name = property.name
783
- const value = name && (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) ? name.text : undefined
784
- if (value === undefined) throw sourceNodeError(property, sourceFile, "clsx() object keys must be identifiers or literals")
785
- return factory.createConditionalExpression(property.initializer, undefined, factory.createStringLiteral(value), undefined, factory.createStringLiteral(""))
786
- }))
787
- throw sourceNodeError(node, sourceFile, "clsx() arguments must be string/number literals, literal arrays, literal objects, or conditionals")
509
+ function cssStringEnd(css, start) {
510
+ const quote = css[start]
511
+ let index = start + 1
512
+ while (index < css.length) {
513
+ if (css[index] === "\\") index += 2
514
+ else if (css[index++] === quote) break
515
+ else if (css[index - 1] === "\n") break
788
516
  }
789
- const combine = entries => entries.length ? entries.reduce((result, entry) => factory.createBinaryExpression(factory.createBinaryExpression(result, factory.createToken(ts.SyntaxKind.PlusToken), factory.createStringLiteral(" ")), factory.createToken(ts.SyntaxKind.PlusToken), entry)) : factory.createStringLiteral("")
517
+ return index
518
+ }
790
519
 
791
- const visitor = node => {
792
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && names.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) return combine(node.arguments.map(lower))
793
- if (ts.isIdentifier(node) && names.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "clsx imports may only be called directly")
794
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "clsx") {
795
- const clause = node.importClause
796
- if (!clause || clause.isTypeOnly) return node
797
- let bindings = clause.namedBindings
798
- if (bindings && ts.isNamedImports(bindings)) {
799
- const elements = bindings.elements.filter(entry => entry.isTypeOnly || (entry.propertyName ?? entry.name).text !== "clsx")
800
- bindings = elements.length ? factory.updateNamedImports(bindings, elements) : undefined
801
- }
802
- const defaultName = clause.name && names.has(clause.name.text) ? undefined : clause.name
803
- if (!defaultName && !bindings) return undefined
804
- return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, defaultName, bindings), node.moduleSpecifier, node.attributes)
520
+ function maskCssCommentsAndStrings(css) {
521
+ const masked = [...css]
522
+ let index = 0
523
+ while (index < css.length) {
524
+ let end
525
+ if (css.startsWith("/*", index)) {
526
+ const close = css.indexOf("*/", index + 2)
527
+ end = close === -1 ? css.length : close + 2
528
+ } else if (css[index] === '"' || css[index] === "'") {
529
+ end = cssStringEnd(css, index)
530
+ } else {
531
+ index++
532
+ continue
805
533
  }
806
- return ts.visitEachChild(node, visitor, context)
534
+ for (; index < end; index++) if (masked[index] !== "\n") masked[index] = " "
807
535
  }
808
- return ts.visitNode(sourceFile, visitor)
536
+ return masked.join("")
809
537
  }
810
538
 
811
- function normalizeLazyStateInitializers(sourceFile, factory, context, file, sourceFiles, sourceIndex) {
812
- const bindings = new Set()
813
- for (const statement of sourceFile.statements) {
814
- if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || !["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text)) continue
815
- const named = statement.importClause?.namedBindings
816
- if (named && ts.isNamedImports(named)) for (const entry of named.elements) {
817
- const imported = (entry.propertyName ?? entry.name).text
818
- if (!entry.isTypeOnly && ["useReducer", "useState"].includes(imported) && entry.name.text === imported) bindings.add(imported)
819
- }
820
- }
821
- if (!bindings.size) return sourceFile
822
- const imports = clientImportBindings(sourceFile, file, sourceFiles)
823
- const visitor = node => {
824
- if (bindings.has("useReducer") && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useReducer" && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments.length === 3) {
825
- const initialArg = node.arguments[1]
826
- const initializer = node.arguments[2]
827
- let declaration
828
- if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) declaration = initializer
829
- else if (ts.isIdentifier(initializer)) {
830
- declaration = localComponentDeclaration(sourceFile, initializer.text)
831
- const binding = imports.get(initializer.text)
832
- if (!declaration && binding && binding.kind !== "namespace") {
833
- try {
834
- declaration = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles)
835
- } catch {}
836
- }
539
+ function normalizeStyles(value, base) {
540
+ if (value === undefined) return { urls: [], sources: [] }
541
+ if (!Array.isArray(value)) throw new Error("kudzu.config styles must be an array")
542
+ const urls = []
543
+ const sources = []
544
+ for (let index = 0; index < value.length; index++) {
545
+ const style = value[index]
546
+ const label = `kudzu.config styles[${index}]`
547
+ if (typeof style === "string") {
548
+ if (!style) throw new Error(`${label} must be a non-empty URL`)
549
+ if (style.startsWith("//")) throw new Error(`${label} must be root-relative or an absolute HTTP URL`)
550
+ if (style.startsWith("/")) {
551
+ urls.push(withBase(base, style))
552
+ continue
837
553
  }
838
- if (!declaration || declaration.parameters.length !== 1 || !ts.isIdentifier(declaration.parameters[0].name) || declaration.parameters[0].initializer || declaration.parameters[0].dotDotDotToken || declaration.asteriskToken || declaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() requires one inline, same-file, or relative-imported synchronous one-parameter initializer")
839
- if (!isSerializableStateLiteral(initialArg)) throw sourceNodeError(initialArg, sourceFile, "Lazy useReducer() initial argument must be directly serializable")
840
- const expression = reactMemoExpression(declaration)
841
- const lowered = expression && substituteClone(expression, new Map([[declaration.parameters[0].name.text, initialArg]]), factory, context)
842
- if (!lowered || !isSerializableStateLiteral(lowered)) throw sourceNodeError(initializer, sourceFile, "Lazy useReducer() initializer must directly return a serializable primitive, plain-object, or array literal derived only from its initial argument")
843
- return factory.updateCallExpression(node, node.expression, node.typeArguments, [node.arguments[0], synthesizeSerializableStateLiteral(lowered, factory)])
844
- }
845
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && bindings.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile) && node.arguments[0] && (ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) {
846
- const initializer = node.arguments[0]
847
- if (node.arguments.length !== 1 || initializer.parameters.length || initializer.asteriskToken || initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || ts.isFunctionExpression(initializer) && initializer.name) throw sourceNodeError(initializer, sourceFile, "Lazy useState() requires one anonymous synchronous zero-parameter initializer")
848
- const expression = ts.isBlock(initializer.body)
849
- ? initializer.body.statements.length === 1 && ts.isReturnStatement(initializer.body.statements[0]) ? initializer.body.statements[0].expression : undefined
850
- : initializer.body
851
- if (!expression || !isSerializableStateLiteral(expression)) throw sourceNodeError(initializer.body, sourceFile, "Lazy useState() initializer must return one directly serializable primitive, plain-object, or array literal")
852
- return factory.updateCallExpression(node, node.expression, node.typeArguments, [cloneAst(expression, factory, context)])
554
+ if (!/^https?:\/\//i.test(style)) throw new Error(`${label} must be root-relative or an absolute HTTP URL`)
555
+ try { new URL(style) } catch { throw new Error(`${label} must be root-relative or an absolute HTTP URL`) }
556
+ urls.push(style)
557
+ continue
853
558
  }
854
- return ts.visitEachChild(node, visitor, context)
559
+ if (!isPlainRecord(style) || Object.keys(style).some(key => !["source", "output", "transform"].includes(key))) throw new Error(`${label} must be a URL or a source style object`)
560
+ if (typeof style.source !== "string" || !style.source) throw new Error(`${label}.source must be a non-empty file path`)
561
+ if (typeof style.output !== "string" || !style.output.startsWith("/") || style.output.startsWith("//") || /[%?#\\\0]/.test(style.output) || style.output.split("/").includes("..") || !style.output.endsWith(".css")) throw new Error(`${label}.output must be a root-relative .css path without query, hash, or traversal`)
562
+ if (style.transform !== undefined && typeof style.transform !== "function") throw new Error(`${label}.transform must be a function`)
563
+ const entry = { label, source: resolve(root, style.source), output: style.output, transform: style.transform }
564
+ sources.push(entry)
565
+ urls.push(withBase(base, style.output))
855
566
  }
856
- return ts.visitNode(sourceFile, visitor)
567
+ return { urls, sources }
857
568
  }
858
569
 
859
- function normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex }) {
860
- const factory = context.factory
861
- let customHookTimerStates = new Set()
862
- sourceFile = applyNormalizationPasses(sourceFile, [
863
- ...(importedStaticCollections ? [source => normalizeImportedStaticCollections(source, importedStaticCollections, factory, context)] : []),
864
- source => normalizeReactRouterSyntax(source, factory, context, base),
865
- source => normalizeClsxSyntax(source, factory, context),
866
- source => normalizeMediaQueryExternalStores(source, factory, context),
867
- source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
868
- source => normalizeNavigatorCapabilityConditions(source, factory, context),
869
- source => normalizeEffectAnimationFrameRefs(source, factory, context),
870
- source => {
871
- const result = normalizeCustomHookTimerRefs(source, factory, context)
872
- customHookTimerStates = result.timerStates
873
- return result.sourceFile
874
- },
875
- source => {
876
- validateUseIdSyntax(source)
877
- return source
878
- },
879
- source => normalizeLazyStateInitializers(source, factory, context, file, sourceFiles, sourceIndex),
880
- source => normalizeZustandMigrationSyntax(source, factory, context),
881
- source => normalizeRenderControlFlow(source, factory, context),
882
- source => {
883
- workerCompiler.rejectOrdinaryImports(source, file, sourceFiles)
884
- return source
885
- }
886
- ])
887
- return { sourceFile, customHookTimerStates }
570
+ function normalizePublicDirectory(value) {
571
+ if (value === undefined) return join(root, "public")
572
+ if (typeof value !== "string" || !value) throw new Error("kudzu.config publicDir must be a non-empty directory path")
573
+ const directory = resolve(root, value)
574
+ if (directory === outputDirectory || directory === workDirectory) throw new Error("kudzu.config publicDir cannot be dist or .kudzu")
575
+ return directory
888
576
  }
889
577
 
890
- function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base }) {
891
- const { moduleIR } = semantic
892
- return context => sourceFile => {
893
- const hasLinkElements = /<link/i.test(sourceFile.text)
894
- const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
895
- const importedCollections = new Set(importedStaticCollections.keys())
896
- const normalized = normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex })
897
- sourceFile = normalized.sourceFile
898
- const { customHookTimerStates } = normalized
899
- const factory = context.factory
900
- const sourceName = source => relative(root, source.fileName).replaceAll(sep, "/")
901
- const componentAnalysis = createComponentAnalysisSession(semantic.componentAnalysis)
902
- const descriptors = createDescriptorSession({
903
- semantic,
904
- handlerUrl,
905
- factory,
906
- context,
907
- compileEventCommand,
908
- handlerLowering,
909
- isPrimitiveLiteral: isPrimitiveDefaultLiteral,
910
- sourceName,
911
- rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
912
- })
913
- const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
914
- const packageBindings = packageImportBindings(sourceFile)
915
- for (const [name] of packageBindings) {
916
- const references = referenceIdentifiers(sourceFile, name)
917
- const invalid = references.find(reference => !insideJsxEventHandler(reference, sourceFile))
918
- if (invalid) throw sourceNodeError(invalid, sourceFile, `Package import ${JSON.stringify(name)} may only be referenced directly inside JSX event handlers`)
919
- }
920
- const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
921
- const importedSourceCache = new Map()
922
- const importedSource = target => {
923
- let result = importedSourceCache.get(target)
924
- if (!result) {
925
- result = normalizeCompilerSource(parseSourceFile(target, sourceIndex.get(target)), { base, context, file: target, sourceFiles, sourceIndex })
926
- importedSourceCache.set(target, result)
927
- }
928
- return result.sourceFile
929
- }
930
- const importedCollectionTransforms = new Map()
931
- const importedCalculationFunctions = new Map()
932
- for (const [name, binding] of importBindings) {
933
- if (binding.kind === "namespace") continue
934
- try {
935
- importedCollectionTransforms.set(name, resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles))
936
- } catch {}
937
- }
938
- const settersByFunction = new Map()
939
- const stateOwnersByFunction = new Map()
940
- const localStateSettersByFunction = new Map()
941
- const reducersByFunction = new Map()
942
- const zustandStores = new Map()
943
- const resolvedZustandStore = entry => {
944
- const exportName = entry.kind === "default" ? "default" : entry.imported
945
- const key = `${entry.target}:${exportName}`
946
- if (zustandStores.has(key)) return zustandStores.get(key)
947
- const targetSource = parseSourceFile(entry.target, sourceIndex.get(entry.target))
948
- const store = analyzeZustandStores(targetSource).get(exportName)
949
- zustandStores.set(key, store)
950
- return store
951
- }
952
- const functions = new Map()
953
- const customHookFunctionsByOwner = new Map()
954
- const customHookPrivateFields = new WeakMap()
955
- const components = new Map()
956
- const contexts = new Set()
957
- const customHooks = new Map()
958
- const jsxLocalDeclarations = new Map()
959
- const jsxLocalsByFunction = new Map()
960
- const listLocalDeclarations = []
961
- const listLocalUses = []
962
- const analysisSource = node => {
963
- const original = ts.getOriginalNode(node)
964
- return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
965
- }
966
- const analyzedProps = owner => {
967
- if (owner.parameters.length !== 1 || !ts.isObjectBindingPattern(owner.parameters[0].name)) return []
968
- return owner.parameters[0].name.elements.map(element => ({
969
- name: (element.propertyName ?? element.name).getText(),
970
- local: element.name.getText(),
971
- ...(element.dotDotDotToken ? { rest: true } : {}),
972
- ...(element.initializer ? { hasDefault: true } : {})
973
- }))
974
- }
975
- const ownerName = owner => owner.name?.text ?? (ts.isVariableDeclaration(owner.parent) && ts.isIdentifier(owner.parent.name) ? owner.parent.name.text : "anonymous")
976
- const ensureOwner = (owner, kind = "component") => componentAnalysis.registerOwner(owner, { kind, name: ownerName(owner), props: analyzedProps(owner), source: analysisSource(owner) })
977
- const registerState = (owner, state, setter, kind, node, externalOwner) => {
978
- const ownerRecord = ensureOwner(owner)
979
- const stateOwner = externalOwner ?? `owner:${ownerRecord.slot}`
980
- const stateOwners = stateOwnersByFunction.get(owner) ?? new Map()
981
- stateOwners.set(state, stateOwner)
982
- stateOwnersByFunction.set(owner, stateOwners)
983
- return componentAnalysis.registerState(owner, { name: state, setter, kind, ...(externalOwner ? { owner: externalOwner } : {}), source: analysisSource(node) })
984
- }
985
- const stateOwnersForNode = node => {
986
- for (let current = node.parent; current; current = current.parent) {
987
- if (isFunctionLike(current) && stateOwnersByFunction.has(current)) return stateOwnersByFunction.get(current)
988
- }
989
- return new Map()
990
- }
991
- const fallbackOwner = node => {
992
- for (let current = node.parent; current; current = current.parent) {
993
- const owner = isFunctionLike(current) ? componentAnalysis.owner(current) : undefined
994
- if (owner) return `owner:${owner.slot}`
995
- }
996
- return "module"
997
- }
998
- let usesBehavior = false
999
- let usesBinding = false
1000
- let usesConditional = false
1001
- let usesList = false
1002
- let usesListEffects = false
1003
- let usesListItem = false
1004
- let usesRowState = false
1005
- let usesRowRef = false
1006
- let usesComponentState = false
1007
- let usesComponentId = false
1008
- let usesComponentRef = false
1009
- let usesComponentEffects = false
578
+ async function resolveDocumentMetadata(value, context, label) {
579
+ if (value === undefined) return {}
580
+ const metadata = typeof value === "function" ? await value(context) : value
581
+ if (!isPlainRecord(metadata)) throw new Error(`${label} must be a plain object or a function returning one`)
582
+ return metadata
583
+ }
1010
584
 
1011
- const resolveContextHook = (returned, hookSource) => {
1012
- if (!hasFrameworkImport(hookSource, "useContext")) throw sourceNodeError(returned.expression, hookSource, "Relative Context hooks must call useContext imported from react or @kudzujs/core")
1013
- if (returned.arguments.length !== 1 || !ts.isIdentifier(returned.arguments[0])) throw sourceNodeError(returned, hookSource, "Relative Context hooks must directly return useContext(ContextIdentifier)")
1014
- const contextName = returned.arguments[0].text
1015
- let providerSource = hookSource
1016
- let providerContextName = contextName
1017
- const hookImports = clientImportBindings(hookSource, hookSource.fileName, sourceFiles)
1018
- if (hookImports.has(contextName)) {
1019
- const binding = hookImports.get(contextName)
1020
- if (binding.kind === "namespace" || binding.kind === "default") throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a named Context import")
1021
- providerSource = importedSource(binding.target)
1022
- providerContextName = binding.imported
1023
- }
1024
- const hasContext = hasFrameworkImport(providerSource, "createContext") && providerSource.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === providerContextName && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "createContext"))
1025
- if (!hasContext) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a local or named relative createContext() declaration")
585
+ export function normalizeNavigation(value) {
586
+ if (value === undefined) return []
587
+ if (!isPlainRecord(value)) throw new Error("kudzu.config navigation must be a plain object")
588
+ if (Object.keys(value).some(key => !["routes", "groups"].includes(key))) throw new Error("kudzu.config navigation only supports routes or groups")
589
+ if ((value.routes === undefined) === (value.groups === undefined)) throw new Error("kudzu.config navigation must define exactly one of routes or groups")
590
+ const inputs = value.routes === undefined ? value.groups : [{ routes: value.routes }]
591
+ if (!Array.isArray(inputs) || !inputs.length) throw new Error("kudzu.config navigation.groups must be a nonempty array")
592
+ const groups = inputs.map((group, groupIndex) => {
593
+ const label = value.routes === undefined ? `kudzu.config navigation.groups[${groupIndex}]` : "kudzu.config navigation"
594
+ if (!isPlainRecord(group)) throw new Error(`${label} must be a plain object`)
595
+ if (Object.keys(group).some(key => key !== "routes")) throw new Error(`${label} only supports routes`)
596
+ if (!Array.isArray(group.routes) || !group.routes.length) throw new Error(`${label}.routes must be a nonempty array`)
597
+ const routes = normalizeNavigationRoutes(group.routes, `${label}.routes`)
598
+ const id = createHash("sha256").update(JSON.stringify([...routes].sort())).digest("hex").slice(0, 16)
599
+ return { label, index: groupIndex, routes, routeSet: new Set(routes), id, assetName: value.routes === undefined ? `kudzu-navigation-${id}.js` : "kudzu-navigation.js" }
600
+ })
601
+ const identities = groups.flatMap(group => group.routes.map(route => [route, group.label]))
602
+ const seenRoutes = new Map()
603
+ for (const [route, label] of identities) {
604
+ if (seenRoutes.has(route)) throw new Error(`${label} route ${JSON.stringify(route)} duplicates ${seenRoutes.get(route)}`)
605
+ seenRoutes.set(route, label)
606
+ }
607
+ const seenAssets = new Map()
608
+ for (const group of groups) {
609
+ if (seenAssets.has(group.assetName)) throw new Error(`${group.label} navigation hash/asset collision with ${seenAssets.get(group.assetName)}`)
610
+ seenAssets.set(group.assetName, group.label)
611
+ }
612
+ return groups
613
+ }
1026
614
 
1027
- const providers = []
1028
- const findProviders = node => {
1029
- if (ts.isJsxAttribute(node) && node.name.text === "value") {
1030
- const element = node.parent?.parent
1031
- const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
1032
- if (ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && tag.expression.text === providerContextName) providers.push(node)
1033
- }
1034
- ts.forEachChild(node, findProviders)
1035
- }
1036
- findProviders(providerSource)
1037
- if (providers.length !== 1) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require exactly one Provider value in the Context module")
1038
- const provider = providers[0]
1039
- const value = provider.initializer && ts.isJsxExpression(provider.initializer) && provider.initializer.expression ? unwrapExpression(provider.initializer.expression) : undefined
1040
- if (!value || !ts.isObjectLiteralExpression(value)) throw sourceNodeError(provider, providerSource, "Context Provider value must be one direct object literal")
1041
- const owner = nearestFunction(provider)
1042
- if (!owner) throw sourceNodeError(provider, providerSource, "Context Provider value must be returned by a component")
1043
- const stateOwner = `external:${sourceName(providerSource)}:${owner.getStart(providerSource)}`
615
+ function normalizeNavigationRoutes(values, label) {
616
+ const routes = values.map((route, index) => {
617
+ if (typeof route !== "string" || !route.startsWith("/") || route.startsWith("//") || /[?#\\\0]/.test(route) || /%(?:2f|5c)/i.test(route)) throw new Error(`${label}[${index}] must be a root-relative path without query, hash, or traversal`)
618
+ let decoded
619
+ try { decoded = decodeURIComponent(route) } catch { throw new Error(`${label}[${index}] must be a root-relative path without query, hash, or traversal`) }
620
+ if (decoded.split("/").includes("..") || /[?#\\\0]/.test(decoded)) throw new Error(`${label}[${index}] must be a root-relative path without query, hash, or traversal`)
621
+ return route
622
+ })
623
+ if (new Set(routes).size !== routes.length) throw new Error(`${label} must contain unique paths`)
624
+ return routes
625
+ }
1044
626
 
1045
- const states = new Map()
1046
- const callbacks = new Map()
1047
- const hasUseState = hasFrameworkImport(providerSource, "useState")
1048
- const collectProviderBindings = node => {
1049
- if (ts.isVariableDeclaration(node) && nearestFunction(node) === owner) {
1050
- if (hasUseState && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "useState") {
1051
- const [state, setter] = node.name.elements
1052
- if (node.name.elements.length === 2 && state && setter && ts.isBindingElement(state) && ts.isBindingElement(setter) && ts.isIdentifier(state.name) && ts.isIdentifier(setter.name)) states.set(setter.name.text, state.name.text)
1053
- }
1054
- if (ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) callbacks.set(node.name.text, node.initializer)
1055
- }
1056
- ts.forEachChild(node, collectProviderBindings)
1057
- }
1058
- collectProviderBindings(owner.body)
1059
-
1060
- const fields = new Set()
1061
- const stateFields = new Set([...states].flat())
1062
- for (const property of value.properties) {
1063
- if (!ts.isShorthandPropertyAssignment(property)) throw sourceNodeError(property, providerSource, "Context Provider values must use direct shorthand state, setter, or action fields")
1064
- const name = property.name.text
1065
- if (!stateFields.has(name) && !callbacks.has(name)) throw sourceNodeError(property, providerSource, `Context Provider field ${JSON.stringify(name)} must be a direct provider-owned state, setter, or action`)
1066
- fields.add(name)
1067
- }
1068
- for (const [setter, state] of states) {
1069
- if (fields.has(setter) !== fields.has(state)) throw sourceNodeError(value, providerSource, `Context Provider state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be exposed together`)
1070
- }
1071
- for (const [name, callback] of callbacks) {
1072
- if (!fields.has(name)) continue
1073
- if (callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} must be synchronous`)
1074
- const capture = nativeCaptureNames(callback, states).values().next().value
1075
- if (capture) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
1076
- for (const state of referencedStateNames(callback.body, states, callback)) {
1077
- const setter = [...states].find(([, candidate]) => candidate === state)?.[0]
1078
- if (!setter || !fields.has(state) || !fields.has(setter)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} requires exposed state and setter fields for ${JSON.stringify(state)}`)
1079
- }
1080
- }
1081
- return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), stateOwner, states }
1082
- }
1083
-
1084
- const resolveCustomHook = (binding, call) => {
1085
- const exportName = binding.kind === "default" ? "default" : binding.imported
1086
- const key = `${binding.target}:${exportName}`
1087
- if (customHooks.has(key)) return customHooks.get(key)
1088
- const hook = resolveComponentExport(binding.target, exportName, importedSource, sourceFiles)
1089
- const hookSource = hook.getSourceFile()
1090
- if (hook.parameters.length || hook.asteriskToken || hook.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(hook.body)) throw sourceNodeError(hook, hookSource, "Relative custom hooks must be synchronous zero-argument functions with a block body")
1091
- const returns = hook.body.statements.filter(ts.isReturnStatement)
1092
- const returned = returns.length === 1 && returns[0] === hook.body.statements.at(-1) && returns[0].expression ? unwrapExpression(returns[0].expression) : undefined
1093
- if (returned && ts.isCallExpression(returned) && ts.isIdentifier(returned.expression) && returned.expression.text === "useContext") {
1094
- const analysis = resolveContextHook(returned, hookSource)
1095
- customHooks.set(key, analysis)
1096
- return analysis
1097
- }
1098
- if (!returned || !ts.isObjectLiteralExpression(returned)) throw sourceNodeError(hook.body, hookSource, "Relative custom hooks must end with one direct object return or direct useContext(ContextIdentifier)")
1099
-
1100
- const states = new Map()
1101
- const callbacks = new Map()
1102
- for (const statement of hook.body.statements) {
1103
- if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) continue
1104
- for (const declaration of statement.declarationList.declarations) {
1105
- if (ts.isArrayBindingPattern(declaration.name) && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
1106
- const [state, setter] = declaration.name.elements
1107
- if (declaration.name.elements.length === 2 && state && setter && ts.isBindingElement(state) && ts.isBindingElement(setter) && ts.isIdentifier(state.name) && ts.isIdentifier(setter.name)) states.set(setter.name.text, state.name.text)
1108
- }
1109
- if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
1110
- }
1111
- }
1112
- const fields = new Set()
1113
- for (const property of returned.properties) {
1114
- if (!ts.isShorthandPropertyAssignment(property)) throw sourceNodeError(property, hookSource, "Relative custom hooks must return direct shorthand bindings")
1115
- fields.add(property.name.text)
1116
- }
1117
- for (const [name, callback] of callbacks) {
1118
- const capture = nativeCaptureNames(callback, states).values().next().value
1119
- if (capture) throw sourceNodeError(callback, hookSource, `Relative custom hook callback ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
1120
- }
1121
- const privateStates = new Set([...states.values()].filter(state => importedSourceCache.get(hookSource.fileName)?.customHookTimerStates.has(state)))
1122
- const analysis = { callbacks, fields, privateStates, states }
1123
- customHooks.set(key, analysis)
1124
- return analysis
1125
- }
1126
-
1127
- const collect = node => {
1128
- if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
1129
- const callName = ts.isIdentifier(node.initializer.expression) ? node.initializer.expression.text : ""
1130
- if (callName && /^use[A-Z]/.test(callName) && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace" && !resolvedZustandStore(importBindings.get(callName))) {
1131
- if (!isLocalConst(node) || !ts.isObjectBindingPattern(node.name) || node.initializer.arguments.length) throw sourceNodeError(node, sourceFile, "Relative custom hooks must initialize one top-level const object destructuring with no arguments")
1132
- const hook = resolveCustomHook(importBindings.get(callName), node.initializer)
1133
- const names = new Set()
1134
- for (const element of node.name.elements) {
1135
- if (element.dotDotDotToken || element.propertyName || element.initializer || !ts.isIdentifier(element.name)) throw sourceNodeError(element, sourceFile, "Relative custom hook results must use direct identifier shorthand without aliases, defaults, or rest")
1136
- const name = element.name.text
1137
- if (!hook.fields.has(name)) throw sourceNodeError(element, sourceFile, `Relative custom hook does not directly return ${JSON.stringify(name)}`)
1138
- names.add(name)
1139
- }
1140
- const owner = nearestFunction(node)
1141
- if (!owner) throw sourceNodeError(node, sourceFile, "Relative custom hooks cannot be used outside a Kudzu component")
1142
- const setters = settersByFunction.get(owner) ?? new Map()
1143
- const requiredContextStates = new Set()
1144
- if (hook.context) {
1145
- for (const name of names) {
1146
- const callback = hook.callbacks.get(name)
1147
- if (callback) for (const state of referencedStateNames(callback.body, hook.states, callback)) requiredContextStates.add(state)
1148
- }
1149
- }
1150
- for (const [setter, state] of hook.states) {
1151
- if (hook.context) {
1152
- if (names.has(setter) && !names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative Context setter ${JSON.stringify(setter)} requires state ${JSON.stringify(state)} to be destructured`)
1153
- if (!names.has(state) && !requiredContextStates.has(state)) continue
1154
- const localSetter = names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`
1155
- setters.set(localSetter, state)
1156
- registerState(owner, state, localSetter, "context", node, hook.stateOwner)
1157
- if (requiredContextStates.has(state)) {
1158
- const fields = customHookPrivateFields.get(node) ?? []
1159
- for (const field of [state, setter]) {
1160
- if (names.has(field) || fields.includes(field)) continue
1161
- const conflict = owner.parameters.some(parameter => bindingNames(parameter.name).includes(field)) || owner.body.statements.some(statement => statement !== node.parent.parent && statementDeclaresName(statement, field))
1162
- if (conflict) throw sourceNodeError(node.name, sourceFile, `Context action state field ${JSON.stringify(field)} conflicts with a consumer binding`)
1163
- fields.push(field)
1164
- }
1165
- customHookPrivateFields.set(node, fields)
1166
- }
1167
- continue
1168
- }
1169
- if (hook.privateStates.has(state)) {
1170
- setters.set(setter, state)
1171
- registerState(owner, state, setter, "custom-hook", node)
1172
- const fields = customHookPrivateFields.get(node) ?? []
1173
- fields.push(state, setter)
1174
- customHookPrivateFields.set(node, fields)
1175
- continue
1176
- }
1177
- if (names.has(setter) !== names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be destructured together`)
1178
- if (names.has(setter)) {
1179
- setters.set(setter, state)
1180
- registerState(owner, state, setter, "custom-hook", node)
1181
- }
1182
- }
1183
- settersByFunction.set(owner, setters)
1184
- for (const name of names) {
1185
- if (hook.callbacks.has(name)) {
1186
- const callbacks = customHookFunctionsByOwner.get(owner) ?? new Map()
1187
- callbacks.set(name, hook.callbacks.get(name))
1188
- customHookFunctionsByOwner.set(owner, callbacks)
1189
- if (hook.context) {
1190
- const reducers = reducersByFunction.get(owner) ?? new Map()
1191
- reducers.set(name, { contextAction: hook.callbacks.get(name), states: hook.states })
1192
- reducersByFunction.set(owner, reducers)
1193
- }
1194
- }
1195
- else if (![...hook.states].some(([setter, state]) => name === setter || name === state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook result ${JSON.stringify(name)} must be a direct useState value, setter, or callback`)
1196
- }
1197
- }
1198
- if (ts.isIdentifier(node.name) && callName && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace") {
1199
- const storeImport = importBindings.get(callName)
1200
- const store = resolvedZustandStore(storeImport)
1201
- if (store) {
1202
- const selector = node.initializer.arguments[0]
1203
- if (node.initializer.arguments.length !== 1 || !selector || !ts.isArrowFunction(selector) || selector.parameters.length !== 1 || !ts.isIdentifier(selector.parameters[0].name) || !ts.isPropertyAccessExpression(unwrapExpression(selector.body)) || !ts.isIdentifier(unwrapExpression(selector.body).expression) || unwrapExpression(selector.body).expression.text !== selector.parameters[0].name.text) throw sourceNodeError(node.initializer, sourceFile, "Zustand selectors must be direct arrows such as state => state.quantities")
1204
- const selected = unwrapExpression(selector.body).name.text
1205
- const owner = nearestFunction(node)
1206
- if (!owner) throw sourceNodeError(node, sourceFile, "Zustand stores cannot be used outside a Kudzu component")
1207
- const setters = settersByFunction.get(owner) ?? new Map()
1208
- if (selected === store.field) {
1209
- const setter = `__kStoreState_${node.name.text}`
1210
- setters.set(setter, node.name.text)
1211
- registerState(owner, node.name.text, setter, "store", node)
1212
- }
1213
- else if (store.actions.has(selected)) {
1214
- setters.set(node.name.text, node.name.text)
1215
- registerState(owner, node.name.text, node.name.text, "store-action", node)
1216
- const reducers = reducersByFunction.get(owner) ?? new Map()
1217
- reducers.set(node.name.text, { state: node.name.text, store, action: selected })
1218
- reducersByFunction.set(owner, reducers)
1219
- } else throw sourceNodeError(unwrapExpression(selector.body).name, sourceFile, `Zustand store ${JSON.stringify(store.name)} has no supported property ${JSON.stringify(selected)}`)
1220
- settersByFunction.set(owner, setters)
1221
- }
1222
- }
1223
- if (callName === "useReducer") {
1224
- if (!ts.isArrayBindingPattern(node.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
1225
- const [stateElement, dispatchElement] = node.name.elements
1226
- if (node.name.elements.length !== 2 || !stateElement || !dispatchElement || !ts.isBindingElement(stateElement) || !ts.isBindingElement(dispatchElement) || !ts.isIdentifier(stateElement.name) || !ts.isIdentifier(dispatchElement.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
1227
- if (node.initializer.arguments.length !== 2) throw sourceNodeError(node.initializer, sourceFile, "useReducer() requires exactly a reducer and initial value")
1228
- const reducer = node.initializer.arguments[0]
1229
- if (!ts.isIdentifier(reducer) || !importBindings.has(reducer.text) || importBindings.get(reducer.text).kind === "namespace") throw sourceNodeError(reducer, sourceFile, "useReducer() reducers must be default or named imports from relative TypeScript modules")
1230
- const reducerImport = importBindings.get(reducer.text)
1231
- let reducerDeclaration
1232
- try {
1233
- reducerDeclaration = resolveComponentExport(reducerImport.target, reducerImport.kind === "default" ? "default" : reducerImport.imported, importedSource, sourceFiles)
1234
- } catch {
1235
- throw sourceNodeError(reducer, sourceFile, "useReducer() imports must resolve to a statically analyzable reducer function")
1236
- }
1237
- if (reducerDeclaration.parameters.length !== 2 || reducerDeclaration.asteriskToken || reducerDeclaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(reducer, sourceFile, "useReducer() reducers must be synchronous functions with exactly state and action parameters")
1238
- const owner = nearestFunction(node)
1239
- if (!owner) throw sourceNodeError(node, sourceFile, "useReducer() cannot be used outside a Kudzu component")
1240
- const setters = settersByFunction.get(owner) ?? new Map()
1241
- setters.set(dispatchElement.name.text, stateElement.name.text)
1242
- registerState(owner, stateElement.name.text, dispatchElement.name.text, "reducer", node)
1243
- settersByFunction.set(owner, setters)
1244
- const reducers = reducersByFunction.get(owner) ?? new Map()
1245
- reducers.set(dispatchElement.name.text, { state: stateElement.name.text, reducer: reducer.text, import: reducerImport })
1246
- reducersByFunction.set(owner, reducers)
1247
- }
1248
- if (ts.isArrayBindingPattern(node.name)) {
1249
- const [stateElement, setterElement] = node.name.elements
1250
- if (callName === "useState" && stateElement && setterElement && ts.isBindingElement(stateElement) && ts.isBindingElement(setterElement) && ts.isIdentifier(stateElement.name) && ts.isIdentifier(setterElement.name)) {
1251
- const owner = nearestFunction(node)
1252
- if (owner) {
1253
- const setters = settersByFunction.get(owner) ?? new Map()
1254
- setters.set(setterElement.name.text, stateElement.name.text)
1255
- registerState(owner, stateElement.name.text, setterElement.name.text, "state", node)
1256
- settersByFunction.set(owner, setters)
1257
- const localSetters = localStateSettersByFunction.get(owner) ?? new Set()
1258
- localSetters.add(setterElement.name.text)
1259
- localStateSettersByFunction.set(owner, localSetters)
1260
- }
1261
- }
1262
- }
1263
- }
1264
- if (ts.isFunctionDeclaration(node) && node.name) {
1265
- functions.set(node.name.text, node)
1266
- if (node.parent === sourceFile) {
1267
- components.set(node.name.text, { function: node, declaration: node })
1268
- ensureOwner(node)
1269
- }
1270
- }
1271
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
1272
- functions.set(node.name.text, node.initializer)
1273
- if (node.parent?.parent?.parent === sourceFile) {
1274
- components.set(node.name.text, { function: node.initializer, declaration: node })
1275
- ensureOwner(node.initializer)
1276
- }
1277
- }
1278
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
1279
- const owner = nearestFunction(node)
1280
- if (owner && node.initializer.expression.text === "useRef" && node.initializer.arguments.length === 1 && node.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword) {
1281
- ensureOwner(owner)
1282
- componentAnalysis.registerRef(owner, { name: node.name.text, source: analysisSource(node) })
1283
- }
1284
- if (owner && node.initializer.expression.text === "useId" && node.initializer.arguments.length === 0) {
1285
- ensureOwner(owner)
1286
- componentAnalysis.registerId(owner, { name: node.name.text, source: analysisSource(node) })
1287
- }
1288
- }
1289
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "createContext") contexts.add(node.name.text)
1290
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
1291
- const owner = nearestFunction(node)
1292
- const declarations = jsxLocalDeclarations.get(owner) ?? new Map()
1293
- const entries = declarations.get(node.name.text) ?? []
1294
- entries.push({ node, initializer: node.initializer })
1295
- declarations.set(node.name.text, entries)
1296
- jsxLocalDeclarations.set(owner, declarations)
1297
- }
1298
- ts.forEachChild(node, collect)
1299
- }
1300
- collect(sourceFile)
1301
- const functionsForNode = node => {
1302
- const callbacks = customHookFunctionsByOwner.get(nearestFunction(node))
1303
- return callbacks ? new Map([...functions, ...callbacks]) : functions
1304
- }
1305
- for (const [owner, declarations] of jsxLocalDeclarations) {
1306
- const names = new Set()
1307
- let changed = true
1308
- while (changed) {
1309
- changed = false
1310
- for (const [name, entries] of declarations) {
1311
- if (!names.has(name) && entries.some(({ initializer }) => isJsxLocalValue(initializer, names))) {
1312
- names.add(name)
1313
- changed = true
1314
- }
1315
- }
1316
- }
1317
- for (const name of names) {
1318
- const entries = declarations.get(name)
1319
- if (entries.length > 1) {
1320
- const position = sourceFile.getLineAndCharacterOfPosition(entries[1].node.getStart(sourceFile))
1321
- throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Block-scoped JSX local "${name}" must not shadow another local`)
1322
- }
1323
- }
1324
- jsxLocalsByFunction.set(owner, names)
1325
- }
1326
- for (const [owner, declarations] of jsxLocalDeclarations) {
1327
- const setters = settersByFunction.get(owner) ?? new Map()
1328
- for (const [name, entries] of declarations) {
1329
- for (const declaration of entries) {
1330
- const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections, factory, context, importedCollectionTransforms)
1331
- if (!parts) continue
1332
- const uses = []
1333
- const collectUses = node => {
1334
- if (ts.isJsxExpression(node) && node.initializer === undefined && ts.isIdentifier(node.expression) && node.expression.text === name && nearestFunction(node) === owner) uses.push(node)
1335
- ts.forEachChild(node, collectUses)
1336
- }
1337
- collectUses(owner.body)
1338
- const references = identifierReferenceCount(owner.body, name)
1339
- const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
1340
- if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
1341
- if (references !== uses.length) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" may only be used as a JSX child`)
1342
- listLocalDeclarations.push(declaration.node)
1343
- if (uses.length) listLocalUses.push({ node: uses[0], parts })
1344
- }
1345
- }
1346
- }
1347
- const fail = (node, message) => {
1348
- throw sourceNodeError(node, sourceFile, message)
1349
- }
1350
- const validateImportedCalculation = (call, field) => {
1351
- const name = call.expression.text
1352
- let calculation = importedCalculationFunctions.get(name)
1353
- if (!calculation) {
1354
- const binding = importBindings.get(name)
1355
- try {
1356
- calculation = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
1357
- } catch {
1358
- fail(call.expression, "Reactive imported calculations must resolve to a directly exported relative TypeScript function")
1359
- }
1360
- importedCalculationFunctions.set(name, calculation)
1361
- }
1362
- if (calculation.asteriskToken || calculation.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) fail(call.expression, "Reactive imported calculations must be synchronous functions")
1363
- if (calculation.parameters.length !== call.arguments.length) fail(call, "Reactive imported calculations require one direct argument for each declared parameter")
1364
- const returns = ts.isBlock(calculation.body) ? [] : [unwrapExpression(calculation.body)]
1365
- const collectReturns = node => {
1366
- if (node !== calculation.body && isFunctionLike(node)) return
1367
- if (ts.isReturnStatement(node)) returns.push(node.expression ? unwrapExpression(node.expression) : null)
1368
- ts.forEachChild(node, collectReturns)
1369
- }
1370
- if (ts.isBlock(calculation.body)) collectReturns(calculation.body)
1371
- if (ts.isBlock(calculation.body) && !ts.isReturnStatement(calculation.body.statements.at(-1))) fail(call.expression, "Reactive imported calculations must end with an unconditional return")
1372
- if (!returns.length || returns.some(returned => !returned || !ts.isObjectLiteralExpression(returned))) fail(call.expression, "Reactive imported calculations must return a plain object")
1373
- const fieldExists = returns.every(returned => returned.properties.some(property => ts.isSpreadAssignment(property) || !ts.isComputedPropertyName(property.name) && property.name.text === field))
1374
- if (!fieldExists) fail(call.parent, `Reactive imported calculation does not return field ${JSON.stringify(field)}`)
1375
- }
1376
- const validateReactiveJsxExpression = (expression, allowedNames) => {
1377
- const value = unwrapExpression(expression)
1378
- const formatAccess = ts.isCallExpression(value) && !value.questionDotToken && ts.isPropertyAccessExpression(value.expression) && !value.expression.questionDotToken && value.expression.name.text === "format" ? value.expression : undefined
1379
- const formatter = formatAccess && unwrapExpression(formatAccess.expression)
1380
- const constructor = formatter && ts.isNewExpression(formatter) && ts.isPropertyAccessExpression(formatter.expression) && formatter.expression.name.text === "NumberFormat" && ts.isIdentifier(formatter.expression.expression) && formatter.expression.expression.text === "Intl" ? formatter : undefined
1381
- if (!constructor) {
1382
- const validate = node => {
1383
- const current = unwrapExpression(node)
1384
- if (ts.isPropertyAccessExpression(current) && ts.isCallExpression(unwrapExpression(current.expression))) {
1385
- const call = unwrapExpression(current.expression)
1386
- if (ts.isIdentifier(call.expression) && importBindings.has(call.expression.text) && importBindings.get(call.expression.text).kind !== "namespace") {
1387
- validateImportedCalculation(call, current.name.text)
1388
- for (const argument of call.arguments) collectionExpression(argument, { fail: (target, message) => fail(target, message.replace("Rendered collection", "Reactive imported calculation")), stateNames: allowedNames })
1389
- return factory.createNumericLiteral(0)
1390
- }
1391
- }
1392
- return ts.visitEachChild(current, validate, context)
1393
- }
1394
- const normalized = ts.visitNode(value, validate)
1395
- collectionExpression(normalized, { fail: (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), stateNames: allowedNames })
1396
- return
1397
- }
1398
- const intl = constructor.expression.expression
1399
- if (!isUnshadowedGlobal(intl, sourceFile)) fail(intl, "Reactive JSX Intl.NumberFormat requires the unshadowed global Intl object")
1400
- if (constructor.arguments?.length !== 1 || !ts.isStringLiteral(constructor.arguments[0])) fail(constructor, "Reactive JSX Intl.NumberFormat requires exactly one static string locale")
1401
- const rounded = value.arguments.length === 1 ? unwrapExpression(value.arguments[0]) : undefined
1402
- const roundAccess = rounded && ts.isCallExpression(rounded) && !rounded.questionDotToken && rounded.arguments.length === 1 && ts.isPropertyAccessExpression(rounded.expression) && !rounded.expression.questionDotToken && rounded.expression.name.text === "round" && ts.isIdentifier(rounded.expression.expression) && rounded.expression.expression.text === "Math" ? rounded.expression : undefined
1403
- if (!roundAccess) fail(value, "Reactive JSX Intl.NumberFormat format() requires exactly Math.round(expression)")
1404
- if (!isUnshadowedGlobal(roundAccess.expression, sourceFile)) fail(roundAccess.expression, "Reactive JSX Intl.NumberFormat requires the unshadowed global Math object")
1405
- collectionExpression(rounded.arguments[0], { fail: (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), stateNames: allowedNames })
1406
- }
1407
- const resolveReactiveJsxExpression = (expression, owner, setters) => {
1408
- const declarations = jsxLocalDeclarations.get(owner)
1409
- if (!declarations) return expression
1410
- const substitutions = new Map()
1411
- const resolving = []
1412
- const resolve = (name, reference) => {
1413
- if (substitutions.has(name)) return
1414
- const entries = declarations.get(name)
1415
- if (!entries?.length) return
1416
- if (jsxLocalsByFunction.get(owner)?.has(name)) return
1417
- if (entries.length !== 1 || entries[0].node.parent?.parent?.parent !== owner?.body) return
1418
- const cycle = resolving.indexOf(name)
1419
- if (cycle >= 0) fail(reference, `Reactive JSX local cycle: ${[...resolving.slice(cycle), name].join(" -> ")}`)
1420
- resolving.push(name)
1421
- const initializer = entries[0].initializer
1422
- const visit = node => {
1423
- if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, initializer) && declarations.has(node.text)) resolve(node.text, node)
1424
- ts.forEachChild(node, visit)
1425
- }
1426
- visit(initializer)
1427
- substitutions.set(name, substituteClone(initializer, substitutions, factory, context))
1428
- resolving.pop()
1429
- }
1430
- const visit = node => {
1431
- if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression) && declarations.has(node.text)) resolve(node.text, node)
1432
- ts.forEachChild(node, visit)
1433
- }
1434
- visit(expression)
1435
- if (!substitutions.size) return expression
1436
- const expanded = substituteClone(expression, substitutions, factory, context)
1437
- ts.setParentRecursive(expanded, false)
1438
- expanded.parent = expression.parent
1439
- const usedStates = referencedStateNames(expanded, setters)
1440
- if (!usedStates.size) return expression
1441
- const captures = captureNames(expanded, expanded, setters)
1442
- const allowedNames = new Set([...setters.values(), ...captures])
1443
- validateReactiveJsxExpression(expanded, allowedNames)
1444
- return expanded
1445
- }
1446
- const componentSpecializations = new WeakMap()
1447
- const setterHookHelpers = new WeakMap()
1448
- const expandedRowSpecializations = new WeakMap()
1449
- const nestedRowSpecializations = new Map()
1450
- const reducerComponentCalls = new WeakSet()
1451
- const rowHookCalls = []
1452
- const specializedDeclarations = new WeakSet()
1453
- const stateBackedComponentFunctions = new WeakSet()
1454
- const stateBackedComponentRoots = []
1455
- let specializedImportIndex = 0
1456
- const specialize = (call, component, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set(), ownership) => {
1457
- const result = specializeComponentCall(call, component, sourceFile, factory, context, fail, label, allowComponentRoot, ordinaryHooks, ordinaryStateNames)
1458
- const owner = nearestFunction(call)
1459
- const setters = ownership?.setters ?? settersForNode(call, settersByFunction)
1460
- const stateOwners = ownership?.stateOwners ?? stateOwnersForNode(call)
1461
- const callbacks = functionsForNode(call)
1462
- const propSignals = expression => {
1463
- const signals = new Set()
1464
- if (ts.isIdentifier(expression) && setters.has(expression.text)) signals.add(setters.get(expression.text))
1465
- const callback = ts.isIdentifier(expression) ? callbacks.get(expression.text) : undefined
1466
- for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression)) signals.add(state)
1467
- return [...signals].map(name => ({ name, owner: stateOwners.get(name) ?? (owner ? `owner:${ensureOwner(owner).slot}` : "module") }))
1468
- }
1469
- result.analysis = componentAnalysis.registerSpecialization({
1470
- kind: label,
1471
- ...(owner ? { owner: ensureOwner(owner).slot } : {}),
1472
- ...(analysisSource(call) ? { source: analysisSource(call) } : {}),
1473
- props: result.props.map(prop => {
1474
- const expression = result.propExpressions.get(prop.name)
1475
- const signals = expression ? propSignals(expression) : []
1476
- return { ...prop, ...(signals.length ? { signals } : {}) }
1477
- }),
1478
- states: [
1479
- ...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
1480
- ...result.ordinaryStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
1481
- ],
1482
- refs: [...result.rowRefs.map(({ name, source }) => ({ name, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })), ...result.ordinaryRefs.map(({ name, source }) => ({ name, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))],
1483
- ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
1484
- })
1485
- for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
1486
- for (const ref of [...result.rowRefs, ...result.ordinaryRefs]) ref.analysisOwner = `specialization:${result.analysis.slot}`
1487
- return result
1488
- }
1489
- const registerRowHooks = (call, specialization) => {
1490
- if (!specialization.rowStates.length && !specialization.rowRefs.length) return
1491
- let owner
1492
- for (let current = call.parent; current; current = current.parent) {
1493
- if (isFunctionLike(current) && settersByFunction.has(current)) {
1494
- owner = current
1495
- break
1496
- }
1497
- }
1498
- if (!owner) owner = nearestFunction(call)
1499
- const setters = new Map(settersByFunction.get(owner))
1500
- const stateOwners = new Map(stateOwnersByFunction.get(owner))
1501
- for (const state of specialization.rowStates) {
1502
- setters.set(state.setter, state.state)
1503
- stateOwners.set(state.state, state.analysisOwner)
1504
- }
1505
- settersByFunction.set(owner, setters)
1506
- stateOwnersByFunction.set(owner, stateOwners)
1507
- rowHookCalls.push(call)
1508
- usesRowState ||= specialization.rowStates.length > 0
1509
- usesRowRef ||= specialization.rowRefs.length > 0
1510
- }
1511
- const mergeSpecializedImports = (root, componentSource, call, effects = []) => {
1512
- const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
1513
- for (const name of runtimeImportNames(componentSource, false)) if (referenceIdentifiers(root, name).length) fail(call, "Imported specialized component handlers may only use relative TypeScript runtime imports")
1514
- const substitutions = new Map()
1515
- for (const statement of componentSource.statements) {
1516
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !isStaticImport(statement.moduleSpecifier.text)) continue
1517
- const entry = staticImportEntry(statement, componentSource, componentSource.fileName, staticFiles, importedAssets, cssModules, base, factory)
1518
- if (!entry?.name) continue
1519
- if (referenceIdentifiers(root, entry.name).length) substitutions.set(entry.name, entry.value)
1520
- for (const effect of effects) {
1521
- if (effect.source.getSourceFile() !== componentSource) continue
1522
- if (!referenceIdentifiers(effect.call, entry.name).length) continue
1523
- ts.setParentRecursive(effect.call, false)
1524
- effect.call = substituteClone(effect.call, new Map([[entry.name, entry.value]]), factory, context)
1525
- synthesizeTree(effect.call)
1526
- }
1527
- }
1528
- for (const [name, entry] of componentImports) {
1529
- const references = referenceIdentifiers(root, name)
1530
- if (!references.length) continue
1531
- if (references.some(reference => !insideJsxEventHandler(reference, root))) fail(call, `Imported specialized component runtime import "${name}" may only be used inside event handlers`)
1532
- let local
1533
- do local = `__kDispatchImport${specializedImportIndex++}`
1534
- while (importBindings.has(local))
1535
- substitutions.set(name, factory.createIdentifier(local))
1536
- importBindings.set(local, { ...entry, local })
1537
- }
1538
- if (!substitutions.size) return root
1539
- const merged = substituteClone(root, substitutions, factory, context)
1540
- ts.setParentRecursive(merged, false)
1541
- merged.parent = root.parent
1542
- return merged
1543
- }
1544
- const expandReducerCallbacks = (root, componentSource, call) => {
1545
- const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
1546
- const replacements = new WeakMap()
1547
- let count = 0
1548
- for (const [name, entry] of componentImports) {
1549
- if (entry.kind === "namespace") continue
1550
- const nestedCalls = jsxTagUses(root, name).filter(nestedCall => jsxCallHasReducerCallbackProp(nestedCall, reducersForNode(nestedCall, reducersByFunction)))
1551
- if (!nestedCalls.length) continue
1552
- const imported = entry.kind === "default" ? "default" : entry.imported
1553
- let nestedComponent
1554
- try {
1555
- nestedComponent = resolveComponentExport(entry.target, imported, importedSource, sourceFiles)
1556
- } catch {
1557
- fail(nestedCalls[0], "Reducer callback props require a component imported from a relative TypeScript module")
1558
- }
1559
- for (const nestedCall of nestedCalls) {
1560
- const nested = specialize(nestedCall, nestedComponent, "Reducer-callback")
1561
- if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
1562
- nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
1563
- synthesizeTree(nested.root)
1564
- replacements.set(nestedCall, nested.root)
1565
- count++
1566
- }
1567
- }
1568
- if (!count) return root
1569
- const expanded = replaceSpecializedCalls(root, replacements, context)
1570
- ts.setParentRecursive(expanded, false)
1571
- expanded.parent = root.parent
1572
- return expanded
1573
- }
1574
- const staticConditionValue = expression => {
1575
- const value = unwrapExpression(expression)
1576
- if (value.kind === ts.SyntaxKind.TrueKeyword) return true
1577
- if (value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isIdentifier(value) && value.text === "undefined") return false
1578
- if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) return Boolean(value.text)
1579
- if (ts.isNumericLiteral(value)) return Number(value.text) !== 0
1580
- return undefined
1581
- }
1582
- const foldSetterStaticConditions = root => {
1583
- const visit = node => {
1584
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
1585
- const condition = staticConditionValue(node.left)
1586
- if (condition !== undefined) return condition ? ts.visitNode(node.right, visit) : node.left
1587
- }
1588
- if (ts.isConditionalExpression(node)) {
1589
- const condition = staticConditionValue(node.condition)
1590
- if (condition !== undefined) return ts.visitNode(condition ? node.whenTrue : node.whenFalse, visit)
1591
- }
1592
- return ts.visitEachChild(node, visit, context)
1593
- }
1594
- const folded = ts.visitNode(root, visit)
1595
- ts.setParentRecursive(folded, false)
1596
- folded.parent = root.parent
1597
- return folded
1598
- }
1599
- const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners) => {
1600
- root = foldSetterStaticConditions(root)
1601
- const replacements = new WeakMap()
1602
- let count = 0
1603
- const visit = (node, dynamic = false) => {
1604
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
1605
- visit(node.left, dynamic)
1606
- visit(node.right, true)
1607
- return
1608
- }
1609
- if (ts.isConditionalExpression(node)) {
1610
- visit(node.condition, dynamic)
1611
- visit(node.whenTrue, true)
1612
- visit(node.whenFalse, true)
1613
- return
1614
- }
1615
- const tag = jsxTagName(node)
1616
- if (tag && (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase())) {
1617
- if (!ts.isIdentifier(tag)) fail(node, "Nested setter-callback components must use identifier JSX tags")
1618
- const name = tag.text
1619
- let component = localComponentDeclaration(componentSource, name)
1620
- let imported = false
1621
- if (!component) {
1622
- const binding = clientImportBindings(componentSource, componentSource.fileName, sourceFiles).get(name)
1623
- if (!binding || binding.kind === "namespace") fail(node, `Nested setter-callback component ${name} must be declared locally or imported from a relative TypeScript module`)
1624
- component = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
1625
- imported = true
1626
- }
1627
- if (trail.includes(component)) {
1628
- const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
1629
- fail(node, `Nested setter-callback component cycle: ${chain}`)
1630
- }
1631
- const setters = new Map(parentSetters)
1632
- for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
1633
- const stateOwners = new Map(parentStateOwners)
1634
- for (const state of aggregate.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
1635
- if (jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction)).length) fail(node, "Setter callbacks cannot cross a second component boundary")
1636
- const nested = specialize(node, component, "Nested setter-callback", true, true, new Set(setters.values()), { setters, stateOwners })
1637
- if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
1638
- nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners)
1639
- if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
1640
- aggregate.calculations.push(...nested.calculations)
1641
- aggregate.effects.push(...nested.effects)
1642
- aggregate.hookDeclarations.push(...nested.hookDeclarations)
1643
- aggregate.ordinaryStates.push(...nested.ordinaryStates)
1644
- aggregate.ordinaryRefs.push(...nested.ordinaryRefs)
1645
- aggregate.usesComponentId ||= nested.usesComponentId
1646
- replacements.set(node, nested.root)
1647
- count++
1648
- return
1649
- }
1650
- ts.forEachChild(node, child => visit(child, dynamic))
1651
- }
1652
- visit(root)
1653
- if (!count) return root
1654
- const expanded = replaceSpecializedCalls(root, replacements, context)
1655
- ts.setParentRecursive(expanded, false)
1656
- expanded.parent = root.parent
1657
- return expanded
1658
- }
1659
- for (const [name, component] of components) {
1660
- const calls = jsxTagUses(sourceFile, name)
1661
- const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component.function, settersByFunction.get(nearestFunction(call)) ?? new Map()))
1662
- if (!stateBackedCalls.length) continue
1663
- if (isExportedDeclaration(component.declaration)) fail(component.declaration, `State-backed list component ${name} cannot be exported`)
1664
- if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `State-backed list component ${name} may only be referenced as JSX`)
1665
- if (stateBackedCalls.length !== calls.length) fail(component.declaration, `State-backed list component ${name} must receive its mapped prop from local state at every call`)
1666
- for (const call of stateBackedCalls) {
1667
- const specialization = specialize(call, component.function)
1668
- if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1669
- componentSpecializations.set(call, specialization)
1670
- stateBackedComponentRoots.push(specialization.root)
1671
- }
1672
- specializedDeclarations.add(component.declaration)
1673
- stateBackedComponentFunctions.add(component.function)
1674
- }
1675
- for (const [name, binding] of importBindings) {
1676
- if (binding.kind === "namespace") continue
1677
- const calls = jsxTagUses(sourceFile, name)
1678
- if (!calls.some(call => jsxCallHasDirectStateProp(call, settersByFunction.get(nearestFunction(call)) ?? new Map()))) continue
1679
- const imported = binding.kind === "default" ? "default" : binding.imported
1680
- let component
1681
- try {
1682
- component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
1683
- } catch (error) {
1684
- if (error.message.includes("does not export a statically analyzable keyed list component")) continue
1685
- throw error
1686
- }
1687
- const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
1688
- for (const call of stateBackedCalls) {
1689
- const specialization = specialize(call, component)
1690
- if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1691
- componentSpecializations.set(call, specialization)
1692
- stateBackedComponentRoots.push(specialization.root)
1693
- }
1694
- }
1695
- const specializeSetterCallbacks = (call, component, callbackProps, imported) => {
1696
- if (componentSpecializations.has(call)) fail(call, "Setter callback props cannot be combined with another component specialization")
1697
- if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, "Setter-callback components must use one destructured props parameter")
1698
- for (const prop of callbackProps) {
1699
- const element = component.parameters[0].name.elements.find(entry => !entry.dotDotDotToken && (entry.propertyName ?? entry.name).getText() === prop)
1700
- if (!element || !ts.isIdentifier(element.name)) fail(call, `Setter-callback component must destructure callback prop ${JSON.stringify(prop)}`)
1701
- const references = []
1702
- const collectReferences = node => {
1703
- if (ts.isIdentifier(node) && node.text === element.name.text && isReferenceIdentifier(node)) references.push(node)
1704
- ts.forEachChild(node, collectReferences)
1705
- }
1706
- collectReferences(component.body)
1707
- if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
1708
- }
1709
- const specialization = specialize(call, component, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
1710
- if (specialization.hookDeclarations.length || specialization.effects.length) {
1711
- const substitutions = new Map()
1712
- const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
1713
- for (const attribute of attributes.properties) {
1714
- if (!ts.isJsxAttribute(attribute) || !callbackProps.includes(attribute.name.text) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !ts.isIdentifier(attribute.initializer.expression)) continue
1715
- const callback = functionsForNode(attribute).get(attribute.initializer.expression.text)
1716
- if (callback) substitutions.set(attribute.initializer.expression.text, callback)
1717
- }
1718
- if (substitutions.size) {
1719
- specialization.root = substituteClone(specialization.root, substitutions, factory, context)
1720
- for (const effect of specialization.effects) effect.call = substituteClone(effect.call, substitutions, factory, context)
1721
- }
1722
- }
1723
- specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction), stateOwnersForNode(call))
1724
- if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
1725
- if (specialization.hookDeclarations.length || specialization.effects.length) {
1726
- const owner = nearestFunction(call)
1727
- const name = `KSetterComponent${Math.max(0, call.pos)}`
1728
- const effectStatements = specialization.effects.map(entry => {
1729
- const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
1730
- synthesizeTree(effectCall)
1731
- ts.setOriginalNode(effectCall, entry.source)
1732
- return factory.createExpressionStatement(effectCall)
1733
- })
1734
- const helper = factory.createFunctionDeclaration(
1735
- undefined,
1736
- undefined,
1737
- name,
1738
- undefined,
1739
- [],
1740
- undefined,
1741
- factory.createBlock([...specialization.hookDeclarations, ...effectStatements, factory.createReturnStatement(specialization.root)], true)
1742
- )
1743
- ts.setParentRecursive(helper, false)
1744
- helper.parent = owner.body
1745
- const helpers = setterHookHelpers.get(owner.body) ?? []
1746
- helpers.push(helper)
1747
- setterHookHelpers.set(owner.body, helpers)
1748
- const setters = new Map(settersForNode(call, settersByFunction))
1749
- for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
1750
- settersByFunction.set(helper, setters)
1751
- const stateOwners = new Map(stateOwnersForNode(call))
1752
- for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
1753
- stateOwnersByFunction.set(helper, stateOwners)
1754
- usesComponentState ||= specialization.ordinaryStates.length > 0
1755
- usesComponentId ||= specialization.usesComponentId
1756
- usesComponentRef ||= specialization.ordinaryRefs.length > 0
1757
- usesComponentEffects ||= specialization.effects.length > 0
1758
- specialization.root = factory.createJsxSelfClosingElement(factory.createIdentifier(name), undefined, factory.createJsxAttributes([]))
1759
- ts.setParentRecursive(specialization.root, false)
1760
- specialization.root.parent = call.parent
1761
- }
1762
- componentSpecializations.set(call, specialization)
1763
- }
1764
- for (const [name, component] of components) {
1765
- for (const call of jsxTagUses(sourceFile, name)) {
1766
- const callbackProps = jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functionsForNode(call), reducersForNode(call, reducersByFunction))
1767
- if (callbackProps.length) specializeSetterCallbacks(call, component.function, callbackProps, false)
1768
- }
1769
- }
1770
- for (const [name, binding] of importBindings) {
1771
- if (binding.kind === "namespace") continue
1772
- const calls = jsxTagUses(sourceFile, name)
1773
- const callbackCalls = calls.map(call => ({ call, callbackProps: jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functionsForNode(call), reducersForNode(call, reducersByFunction)) })).filter(entry => entry.callbackProps.length)
1774
- if (!callbackCalls.length) continue
1775
- const imported = binding.kind === "default" ? "default" : binding.imported
1776
- let component
1777
- try {
1778
- component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
1779
- } catch {
1780
- fail(callbackCalls[0].call, "Setter callback props require a component imported from a relative TypeScript module")
1781
- }
1782
- for (const { call, callbackProps } of callbackCalls) specializeSetterCallbacks(call, component, callbackProps, true)
1783
- }
1784
- for (const [name, component] of components) {
1785
- const calls = jsxTagUses(sourceFile, name)
1786
- const dispatchCalls = calls.filter(call => jsxCallHasDirectReducerProp(call, reducersForNode(call, reducersByFunction)))
1787
- if (!dispatchCalls.length) continue
1788
- if (isExportedDeclaration(component.declaration)) fail(component.declaration, `Reducer-dispatch component ${name} cannot be exported`)
1789
- if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} may only be referenced as JSX`)
1790
- if (dispatchCalls.length !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} must receive a direct local reducer dispatch at every call`)
1791
- for (const call of dispatchCalls) {
1792
- if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1793
- const specialization = specialize(call, component.function, "Reducer-dispatch")
1794
- registerRowHooks(call, specialization)
1795
- specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
1796
- componentSpecializations.set(call, specialization)
1797
- reducerComponentCalls.add(call)
1798
- }
1799
- specializedDeclarations.add(component.declaration)
1800
- }
1801
- for (const [name, binding] of importBindings) {
1802
- if (binding.kind === "namespace") continue
1803
- const calls = jsxTagUses(sourceFile, name)
1804
- const dispatchCalls = calls.filter(call => jsxCallHasDirectReducerProp(call, reducersForNode(call, reducersByFunction)))
1805
- if (!dispatchCalls.length) continue
1806
- const imported = binding.kind === "default" ? "default" : binding.imported
1807
- let component
1808
- try {
1809
- component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
1810
- } catch {
1811
- fail(dispatchCalls[0], `Reducer dispatch props require a component imported from a relative TypeScript module`)
1812
- }
1813
- const componentSource = component.getSourceFile()
1814
- for (const call of dispatchCalls) {
1815
- if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1816
- const specialization = specialize(call, component, "Reducer-dispatch")
1817
- registerRowHooks(call, specialization)
1818
- specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
1819
- specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
1820
- synthesizeTree(specialization.root)
1821
- componentSpecializations.set(call, specialization)
1822
- reducerComponentCalls.add(call)
1823
- }
1824
- }
1825
- const rawRenderedLists = []
1826
- const collectRenderedLists = node => {
1827
- const specialization = componentSpecializations.get(node)
1828
- if (specialization) {
1829
- collectRenderedLists(specialization.root)
1830
- return
1831
- }
1832
- if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
1833
- const owner = nearestFunction(node)
1834
- const setters = settersForNode(node, settersByFunction)
1835
- const staticCollection = state => [...(localStateSettersByFunction.get(owner) ?? [])].some(setter => setters.get(setter) === state && !referenceIdentifiers(owner.body, setter).length)
1836
- const calculatedCollection = expression => {
1837
- const value = unwrapExpression(expression)
1838
- if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
1839
- const entries = jsxLocalDeclarations.get(nearestFunction(node))?.get(value.expression.text)
1840
- if (!entries?.length) return undefined
1841
- const initializer = entries.length === 1 ? unwrapExpression(entries[0].initializer) : undefined
1842
- if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || !importBindings.has(initializer.expression.text)) return undefined
1843
- if (entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value.expression, `Calculated collection result "${value.expression.text}" must be one top-level immutable local`)
1844
- validateImportedCalculation(initializer, value.name.text)
1845
- const expanded = resolveReactiveJsxExpression(value, nearestFunction(node), setters)
1846
- if (expanded === value || !referencedStateNames(expanded, setters).size) fail(value, "Calculated collection fields must directly depend on local state")
1847
- return expanded
1848
- }
1849
- const parts = listLocalUses.find(entry => entry.node === node)?.parts ?? keyedListParts(node.expression, setters, jsxLocalDeclarations.get(owner), fail, new Set(), importedCollections, factory, context, importedCollectionTransforms, calculatedCollection, staticCollection)
1850
- if (parts) {
1851
- for (const declaration of parts.aliasDeclarations ?? []) if (!listLocalDeclarations.includes(declaration)) listLocalDeclarations.push(declaration)
1852
- rawRenderedLists.push({ node, parts })
1853
- }
1854
- }
1855
- ts.forEachChild(node, collectRenderedLists)
1856
- }
1857
- collectRenderedLists(sourceFile)
1858
- const collectionAliasUses = rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? [])
1859
- const collectionAliasDeclarations = new Set(rawRenderedLists.flatMap(({ parts }) => parts.aliasDeclarations ?? []))
1860
- for (const declaration of collectionAliasDeclarations) {
1861
- const owner = nearestFunction(declaration)
1862
- const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.includes(reference))
1863
- if (unsupported) fail(unsupported, `Rendered collection alias "${declaration.name.text}" may only be used as a rendered collection source`)
1864
- }
1865
- const rejectUnsupportedRenderControl = node => {
1866
- if (ts.isIfStatement(node) && containsRenderControl(node, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
1867
- const setters = settersForNode(node, settersByFunction)
1868
- if (referencedStateNames(node.expression, setters).size) {
1869
- fail(node, "Reactive render if statements must use terminal returns or exhaustive adjacent JSX assignment")
1870
- }
1871
- }
1872
- ts.forEachChild(node, rejectUnsupportedRenderControl)
1873
- }
1874
- rejectUnsupportedRenderControl(sourceFile)
1875
- const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
1876
- const tag = jsxTagName(parts.root)
1877
- return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
1878
- }))
1879
- const keyedComponentCalls = new Set(rawRenderedLists.map(({ parts }) => parts.root))
1880
- for (const call of rowHookCalls) if (!keyedComponentCalls.has(call)) fail(call, "Keyed row hooks are only supported in direct keyed map rows")
1881
- for (const name of listComponentNames) {
1882
- let component = components.get(name)
1883
- const local = Boolean(component)
1884
- if (!component) {
1885
- const binding = importBindings.get(name)
1886
- if (!binding || binding.kind === "namespace") fail(sourceFile, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
1887
- const imported = binding.kind === "default" ? "default" : binding.imported
1888
- component = { function: resolveComponentExport(binding.target, imported, importedSource, sourceFiles), declaration: undefined }
1889
- }
1890
- const declaredCalls = jsxTagUses(sourceFile, name)
1891
- if (local && identifierReferenceCount(sourceFile, name) !== declaredCalls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
1892
- const calls = [...new Set([
1893
- ...declaredCalls.filter(call => !stateBackedComponentFunctions.has(nearestFunction(call))),
1894
- ...stateBackedComponentRoots.flatMap(root => jsxTagUses(root, name))
1895
- ])]
1896
- for (const call of calls) {
1897
- const specialization = reducerComponentCalls.has(call)
1898
- ? componentSpecializations.get(call)
1899
- : specialize(call, component.function, "Keyed list", true)
1900
- registerRowHooks(call, specialization)
1901
- if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
1902
- specialization.component = component.function
1903
- specialization.componentSource = component.function.getSourceFile()
1904
- specialization.imported = !local
1905
- componentSpecializations.set(call, specialization)
1906
- }
1907
- if (local) specializedDeclarations.add(component.declaration)
1908
- }
1909
- const expandKeyedComponents = (root, componentSource, trail = [], aggregate) => {
1910
- const replacements = new WeakMap()
1911
- let count = 0
1912
- const visit = (node, currentAggregate = aggregate) => {
1913
- if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
1914
- const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], specializations: [] }
1915
- for (const argument of node.arguments) visit(argument, nestedAggregate)
1916
- if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
1917
- return
1918
- }
1919
- const tag = jsxTagName(node)
1920
- if (tag && (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase())) {
1921
- if (!ts.isIdentifier(tag)) fail(node, "Keyed list components must use identifier JSX tags")
1922
- const name = tag.text
1923
- let component = localComponentDeclaration(componentSource, name)
1924
- let imported = false
1925
- if (!component) {
1926
- const binding = clientImportBindings(componentSource, componentSource.fileName, sourceFiles).get(name)
1927
- if (!binding || binding.kind === "namespace") fail(node, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
1928
- component = resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, importedSource, sourceFiles)
1929
- imported = true
1930
- }
1931
- if (trail.includes(component)) {
1932
- const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
1933
- fail(node, `Keyed list component cycle: ${chain}`)
1934
- }
1935
- const specialization = specialize(node, component, "Keyed list", true)
1936
- registerRowHooks(node, specialization)
1937
- specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
1938
- if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
1939
- expandedRowSpecializations.set(specialization.root, specialization)
1940
- if (currentAggregate) {
1941
- currentAggregate.specializations ??= []
1942
- currentAggregate.specializations.push(specialization.analysis.slot, ...(specialization.specializations ?? []))
1943
- currentAggregate.effects.push(...specialization.effects)
1944
- currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
1945
- currentAggregate.rowStates.push(...specialization.rowStates)
1946
- currentAggregate.rowRefs.push(...specialization.rowRefs)
1947
- }
1948
- replacements.set(node, specialization.root)
1949
- count++
1950
- return
1951
- }
1952
- ts.forEachChild(node, child => visit(child, currentAggregate))
1953
- }
1954
- visit(root)
1955
- if (!count) return root
1956
- const expanded = replaceSpecializedCalls(root, replacements, context)
1957
- ts.setParentRecursive(expanded, false)
1958
- expanded.parent = root.parent
1959
- return expanded
1960
- }
1961
- const preparedRenderedLists = []
1962
- const prepareListCallback = (callback, root, specialization) => {
1963
- const statements = [...specialization.hookDeclarations]
1964
- if (specialization.effects.length) {
1965
- usesListEffects = true
1966
- statements.push(...specialization.effects.map(entry => {
1967
- const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
1968
- synthesizeTree(call)
1969
- ts.setOriginalNode(call, entry.source)
1970
- return factory.createExpressionStatement(call)
1971
- }))
1972
- }
1973
- if (!statements.length) return callback
1974
- const prepared = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, factory.createBlock([...statements, factory.createReturnStatement(root)], true))
1975
- ts.setParentRecursive(prepared, false)
1976
- prepared.parent = callback.parent
1977
- return prepared
1978
- }
1979
- for (const { node, parts: originalParts } of rawRenderedLists) {
1980
- if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
1981
- const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], ordinaryStates: [] }
1982
- const componentSource = specialization.componentSource ?? sourceFile
1983
- specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
1984
- if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
1985
- if (specialization.root !== originalParts.root) componentSpecializations.set(originalParts.root, specialization)
1986
- const root = specialization.root
1987
- let callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
1988
- originalParts.callback,
1989
- originalParts.callback.modifiers,
1990
- originalParts.callback.typeParameters,
1991
- originalParts.callback.parameters,
1992
- originalParts.callback.type,
1993
- originalParts.callback.equalsGreaterThanToken,
1994
- root
1995
- )
1996
- if (callback !== originalParts.callback) {
1997
- ts.setParentRecursive(callback, false)
1998
- callback.parent = originalParts.callback.parent
1999
- }
2000
- callback = prepareListCallback(callback, root, specialization)
2001
- const parts = {
2002
- ...originalParts,
2003
- root,
2004
- callback,
2005
- specializations: [specialization.analysis?.slot, ...(specialization.specializations ?? [])].filter(slot => slot !== undefined),
2006
- rowStates: [...specialization.rowStates, ...specialization.ordinaryStates],
2007
- rowRefs: specialization.rowRefs,
2008
- analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
2009
- }
2010
- for (const calculation of specialization.calculations) {
2011
- ts.setParentRecursive(calculation, false)
2012
- calculation.parent = callback
2013
- validateListExpression(calculation, parts.item, originalParts.root, fail)
2014
- }
2015
- const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2016
- preparedRenderedLists.push({ node, parts, analysis })
2017
- }
2018
-
2019
- const compileRenderExpression = (expression, anchor) => {
2020
- const parts = conditionalParts(expression)
2021
- if (!parts) return ts.visitNode(expression, visitor)
2022
- const setters = settersForNode(anchor, settersByFunction)
2023
- const usedStates = referencedStateNames(parts.condition, setters)
2024
- const captures = captureNames(parts.condition, parts.condition, setters)
2025
- if (!usedStates.size && !captures.size) return ts.visitEachChild(expression, visitor, context)
2026
- usesBehavior = true
2027
- usesConditional = true
2028
- return descriptors.compileConditional(
2029
- parts.kind,
2030
- parts.condition,
2031
- compileRenderExpression(parts.truthy, anchor),
2032
- compileRenderExpression(parts.falsy, anchor),
2033
- setters
2034
- )
2035
- }
2036
-
2037
- let activeStateOwners
2038
- let activeKeyedBlock
2039
- const visitWithStateOwners = (node, stateOwners) => {
2040
- const previous = activeStateOwners
2041
- activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
2042
- const result = ts.visitNode(node, visitor)
2043
- activeStateOwners = previous
2044
- return result
2045
- }
2046
- const keyedEntry = (entries, node) => entries.find(entry => entry.node === node)
2047
- const compileKeyedBlock = (node, { parts: listParts, analysis }) => {
2048
- usesBehavior = true
2049
- usesList = true
2050
- const blockSlot = moduleIR.keyedBlocks.length
2051
- let listSource = listParts.state
2052
- let collection = { kind: "signal", name: listParts.state?.text }
2053
- if (listParts.calculation) {
2054
- usesBinding = true
2055
- listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings, keyedBlock: blockSlot })
2056
- const exportName = ts.isCallExpression(listSource) && ts.isStringLiteral(listSource.arguments[2]) ? listSource.arguments[2].text : undefined
2057
- collection = { kind: "binding", ...(exportName ? { exportName } : {}) }
2058
- }
2059
- const derived = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node) : undefined
2060
- const parent = activeKeyedBlock?.block
2061
- const rowStates = (listParts.rowStates ?? []).map(state => ({ name: state.state, setter: state.setter, owner: state.analysisOwner, ...(analysisSource(state.source) ? { source: analysisSource(state.source) } : {}) }))
2062
- const rowRefs = (listParts.rowRefs ?? []).map(ref => ({ name: ref.name, owner: ref.analysisOwner, ...(analysisSource(ref.source) ? { source: analysisSource(ref.source) } : {}) }))
2063
- const specializations = [...new Set([...(listParts.specializations ?? []), ...rowStates.map(state => state.owner), ...rowRefs.map(ref => ref.owner)].filter(value => value !== undefined).map(value => typeof value === "string" ? Number(value.slice(value.lastIndexOf(":") + 1)) : value))]
2064
- const block = descriptors.registerKeyedBlock({
2065
- ...(analysisSource(node) ? { source: analysisSource(node) } : {}),
2066
- ...(parent ? { parent: parent.slot } : {}),
2067
- children: [],
2068
- collection,
2069
- key: listParts.keyField,
2070
- ...(listParts.ownerField ? { ownerField: listParts.ownerField } : {}),
2071
- item: listParts.item,
2072
- ...(listParts.index ? { index: listParts.index } : {}),
2073
- indexed: listParts.indexed,
2074
- static: Boolean(listParts.static),
2075
- ...(derived ? { selector: derived.slot } : {}),
2076
- selectorStates: [...(listParts.selectorStates ?? [])],
2077
- specializations,
2078
- rowStates,
2079
- rowRefs
2080
- })
2081
- if (parent) parent.children.push(block.slot)
2082
- const previous = activeKeyedBlock
2083
- activeKeyedBlock = { analysis, block, parts: listParts }
2084
- const callback = visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map())
2085
- activeKeyedBlock = previous
2086
- const arguments_ = [
2087
- listSource,
2088
- block.key === null ? factory.createNull() : factory.createStringLiteral(block.key),
2089
- callback,
2090
- factory.createStringLiteral(block.ownerField ?? ""),
2091
- jsonExpression(derived?.selector ?? listParts.selector ?? [], factory),
2092
- block.indexed ? factory.createTrue() : factory.createFalse()
2093
- ]
2094
- if (block.selectorStates.length || block.static) arguments_.push(factory.createArrayLiteralExpression(block.selectorStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
2095
- if (block.static) arguments_.push(factory.createTrue())
2096
- return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
2097
- }
2098
- const visitor = node => {
2099
- if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
2100
- const privateFields = customHookPrivateFields.get(node)
2101
- return factory.updateVariableDeclaration(node, factory.updateObjectBindingPattern(node.name, [
2102
- ...node.name.elements,
2103
- ...privateFields.map(name => factory.createBindingElement(undefined, undefined, name))
2104
- ]), node.exclamationToken, node.type, node.initializer)
2105
- }
2106
- if (ts.isBlock(node) && setterHookHelpers.has(node)) {
2107
- return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
2108
- }
2109
- if (specializedDeclarations.has(node)) return node
2110
- if (componentSpecializations.has(node)) {
2111
- const specialization = componentSpecializations.get(node)
2112
- const stateOwners = new Map([...stateOwnersForNode(node), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
2113
- return visitWithStateOwners(specialization.root, stateOwners)
2114
- }
2115
-
2116
- if (hasLinkElements && (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) && isStylesheetLink(node)) {
2117
- fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
2118
- }
2119
-
2120
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
2121
- if (!node.importClause) fail(node, "Side-effect React imports are not supported because Kudzu does not load the React runtime")
2122
- if (node.importClause.isTypeOnly) return node
2123
- return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral("@kudzujs/core"), node.attributes)
2124
- }
2125
-
2126
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && packageBindings.size && importDeclarationNames(node).some(name => packageBindings.has(name))) return undefined
2127
-
2128
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
2129
- if (!runtimeModuleReference(node)) return node
2130
- if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
2131
- const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
2132
- return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
2133
- }
2134
-
2135
- if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
2136
- if (!runtimeModuleReference(node)) return node
2137
- const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
2138
- return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
2139
- }
2140
-
2141
- const effectAlias = ts.isCallExpression(node) && ts.isIdentifier(node.expression) ? node.expression.text : undefined
2142
- const listEffect = effectAlias === "__kListUseEffect"
2143
- const specializedEffect = listEffect || effectAlias === "__kComponentUseEffect" ? (() => {
2144
- const source = ts.getOriginalNode(node)
2145
- const sourceFile = source.getSourceFile()
2146
- return { source, sourceFile, imports: clientImportBindings(sourceFile, sourceFile.fileName, sourceFiles) }
2147
- })() : undefined
2148
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && effectAlias === "useEffect" || specializedEffect)) {
2149
- const effectFail = (target, message) => {
2150
- if (specializedEffect) throw sourceNodeError(specializedEffect.source, specializedEffect.sourceFile, message)
2151
- fail(target, message)
2152
- }
2153
- if (node.arguments.length !== 2) effectFail(node, "useEffect() requires exactly a callback and literal dependency array")
2154
- const [callbackArgument, dependencies] = node.arguments
2155
- const effectOwner = nearestFunction(node)
2156
- const resolveEffectFunction = expression => {
2157
- if (!ts.isIdentifier(expression)) return undefined
2158
- const entries = jsxLocalDeclarations.get(effectOwner)?.get(expression.text)
2159
- if (entries?.length !== 1 || entries[0].node.parent?.parent?.parent !== effectOwner?.body) return undefined
2160
- const initializer = entries[0].initializer
2161
- return ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer) ? initializer : undefined
2162
- }
2163
- let callback = ts.isArrowFunction(callbackArgument) || ts.isFunctionExpression(callbackArgument) ? callbackArgument : resolveEffectFunction(callbackArgument)
2164
- if (!callback) effectFail(callbackArgument, "useEffect() callback must be inline or one top-level const function")
2165
- if (ts.isFunctionExpression(callback) && callback.name) effectFail(callback, "useEffect() callback function must be anonymous")
2166
- if (callback.asteriskToken) effectFail(callback, "useEffect() callback cannot be a generator")
2167
- if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
2168
- if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
2169
- const setters = settersForNode(node, settersByFunction)
2170
- const dependencyAnalysis = analyzeEffectDependencies({
2171
- dependencies,
2172
- node,
2173
- listEffect,
2174
- keyedItem: activeKeyedBlock?.parts.item,
2175
- setters,
2176
- localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
2177
- factory,
2178
- fail: effectFail
2179
- })
2180
- const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
2181
- if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
2182
- if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
2183
- const cleanupSubstitutions = new Map()
2184
- const collectNamedCleanups = current => {
2185
- if (current !== callback && isFunctionLike(current)) return
2186
- if (ts.isReturnStatement(current) && current.expression && ts.isIdentifier(unwrapExpression(current.expression))) {
2187
- const cleanup = resolveEffectFunction(unwrapExpression(current.expression))
2188
- if (cleanup) cleanupSubstitutions.set(unwrapExpression(current.expression).text, cleanup)
2189
- }
2190
- ts.forEachChild(current, collectNamedCleanups)
2191
- }
2192
- collectNamedCleanups(callback.body)
2193
- if (cleanupSubstitutions.size) {
2194
- callback = substituteClone(callback, cleanupSubstitutions, factory, context)
2195
- ts.setParentRecursive(callback, false)
2196
- callback.parent = callbackArgument.parent
2197
- }
2198
- const returns = effectReturns(callback)
2199
- if (returns.invalid) effectFail(returns.invalid, "useEffect() return values must be inline cleanup functions")
2200
- const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
2201
- if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
2202
- if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
2203
- validateEffectOwnedBrowserResources(callback, returns, effectFail)
2204
- const callbackSource = specializedEffect?.sourceFile ?? sourceFile
2205
- const callbackFile = callbackSource.fileName
2206
- let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
2207
- if (compiledCallback !== callback) {
2208
- ts.setParentRecursive(compiledCallback, false)
2209
- compiledCallback.parent = callback.parent
2210
- }
2211
- let workers = []
2212
- if (listEffect && callbackFile !== file) {
2213
- const originalCallback = specializedEffect.source.arguments[0]
2214
- workerCompiler.rejectConstructions(originalCallback, callbackSource, "Relative TypeScript Worker construction in imported keyed-row effects is not supported; construct the Worker in a directly compiled page or local component effect")
2215
- } else {
2216
- const rewritten = workerCompiler.rewriteEffect(compiledCallback, callbackFile, callbackSource, sourceFiles, factory, context)
2217
- compiledCallback = rewritten.callback
2218
- workers = rewritten.workers
2219
- }
2220
- const descriptor = descriptors.compileEffectCallback(compiledCallback, {
2221
- setters,
2222
- reducers: reducersForNode(node, reducersByFunction),
2223
- importBindings: specializedEffect?.imports ?? importBindings,
2224
- listItem: dependencyItem,
2225
- keyedBlock: activeKeyedBlock?.block.slot,
2226
- deferValues: true,
2227
- snapshotNested: returns.cleanup,
2228
- liveStates: customHookTimerStates
2229
- })
2230
- usesListItem ||= Boolean(itemDependencies.length && !listEffect)
2231
- usesBehavior = true
2232
- const derivedDependencies = hasDerivedDependency ? dependencyEntries.map(entry => entry.kind === "derived" ? descriptors.registerDerived("expression", entry.expression, entry.states, entry.source) : undefined) : []
2233
- const effectSource = specializedEffect?.source ?? node
2234
- const lexicalOwner = nearestFunction(effectSource)
2235
- const effect = descriptors.registerEffect(descriptor, {
2236
- cleanup: returns.cleanup,
2237
- dependencies: hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived" ? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states] } : { kind: "signal", name: entry.name }) : ordinaryDependencies.map(dependency => ({ kind: "signal", name: dependency.text })),
2238
- subscriptions: (hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies).map(dependency => dependency.text),
2239
- dependencyStates: [...dependencyStates.keys()],
2240
- itemDependencies,
2241
- ownership: {
2242
- kind: activeKeyedBlock ? "keyed" : "component",
2243
- ...(activeKeyedBlock ? { keyedBlock: activeKeyedBlock.block.slot } : {}),
2244
- ...(lexicalOwner ? { component: { name: ownerName(lexicalOwner), ...(analysisSource(lexicalOwner) ? { source: analysisSource(lexicalOwner) } : {}) } } : {})
2245
- },
2246
- workers,
2247
- ...(analysisSource(effectSource) ? { source: analysisSource(effectSource) } : {})
2248
- })
2249
- const dependencyExpressions = effect.dependencies.map(dependency => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependency.name])
2250
- return factory.updateCallExpression(node, node.expression, node.typeArguments, [
2251
- callback,
2252
- factory.createArrayLiteralExpression(effect.subscriptions.map(name => factory.createIdentifier(name))),
2253
- factory.createStringLiteral(handlerUrl),
2254
- factory.createStringLiteral(effect.setup.exportName),
2255
- descriptor.states,
2256
- descriptor.scope,
2257
- factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
2258
- effect.cleanup ? factory.createTrue() : factory.createFalse(),
2259
- factory.createArrayLiteralExpression(effect.itemDependencies.map(field => factory.createStringLiteral(field))),
2260
- hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
2261
- factory.createArrayLiteralExpression(effect.dependencyStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)])))
2262
- ])
2263
- }
2264
-
2265
- if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && ((node.initializer.expression.text === "useState" || node.initializer.expression.text === "__kRowUseState" || node.initializer.expression.text === "__kComponentUseState") && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
2266
- const stateElement = node.name.elements[0]
2267
- if (!stateElement || !ts.isBindingElement(stateElement) || !ts.isIdentifier(stateElement.name)) return node
2268
- const initializer = factory.updateCallExpression(node.initializer, node.initializer.expression, node.initializer.typeArguments, [
2269
- ...node.initializer.arguments,
2270
- factory.createStringLiteral(stateElement.name.text)
2271
- ])
2272
- return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
2273
- }
2274
-
2275
- if (ts.isVariableDeclaration(node) && listLocalDeclarations.includes(node)) {
2276
- return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createIdentifier("undefined"))
2277
- }
2278
-
2279
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && importBindings.has(node.initializer.expression.text)) {
2280
- const setters = settersForNode(node, settersByFunction)
2281
- const stateNames = new Set(setters.values())
2282
- const rewrite = current => {
2283
- if (ts.isShorthandPropertyAssignment(current) && stateNames.has(current.name.text)) return factory.createPropertyAssignment(current.name, factory.createPropertyAccessExpression(current.name, "value"))
2284
- if (ts.isIdentifier(current) && stateNames.has(current.text) && isReferenceIdentifier(current)) return factory.createPropertyAccessExpression(current, "value")
2285
- return ts.visitEachChild(current, rewrite, context)
2286
- }
2287
- if (referencedStateNames(node.initializer, setters).size) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, ts.visitNode(node.initializer, rewrite))
2288
- }
2289
-
2290
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && jsxLocalsByFunction.get(nearestFunction(node))?.has(node.name.text) && referencesIdentifier(nearestFunction(node).body, node.name.text)) {
2291
- const compiled = compileRenderExpression(node.initializer, node)
2292
- if (compiled !== node.initializer) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, compiled)
2293
- }
2294
-
2295
- if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
2296
- const compiled = compileRenderExpression(node.expression, node)
2297
- if (compiled !== node.expression) return factory.updateReturnStatement(node, compiled)
2298
- }
2299
-
2300
- const listCondition = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.conditions ?? [], node.expression) : undefined
2301
- if (listCondition) {
2302
- const entry = listCondition.value
2303
- return factory.updateJsxExpression(node, descriptors.compileListConditional({
2304
- ...entry,
2305
- keyedBlock: activeKeyedBlock.block.slot,
2306
- truthy: ts.visitNode(entry.truthy, visitor),
2307
- falsy: ts.visitNode(entry.falsy, visitor)
2308
- }))
2309
- }
2310
-
2311
- const listValue = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.expression) : undefined
2312
- if (listValue) {
2313
- return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, { ...listValue.value, keyedBlock: activeKeyedBlock.block.slot }))
2314
- }
2315
-
2316
- const attributeListValue = ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.initializer.expression) : undefined
2317
- if (attributeListValue) {
2318
- return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, { ...attributeListValue.value, keyedBlock: activeKeyedBlock.block.slot })))
2319
- }
2320
-
2321
- if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
2322
- const renderedList = keyedEntry(preparedRenderedLists, node)
2323
- const nestedList = keyedEntry(activeKeyedBlock?.analysis.nested ?? [], unwrapExpression(node.expression))
2324
- if (renderedList || nestedList) return compileKeyedBlock(node, renderedList ?? nestedList)
2325
- const conditional = conditionalParts(node.expression)
2326
- if (conditional) {
2327
- const compiled = compileRenderExpression(node.expression, node)
2328
- if (compiled !== node.expression) return factory.updateJsxExpression(node, compiled)
2329
- }
2330
- const setters = settersForNode(node, settersByFunction)
2331
- const expression = resolveReactiveJsxExpression(node.expression, nearestFunction(node), setters)
2332
- const usedStates = referencedStateNames(expression, setters)
2333
- const captures = captureNames(expression, expression, setters)
2334
- if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
2335
- usesBehavior = true
2336
- usesBinding = true
2337
- return factory.updateJsxExpression(node, descriptors.compileReactiveBinding(expression, { setters, importBindings }))
2338
- }
2339
- }
2340
-
2341
- if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !isContextProviderValue(node, contexts) && !/^on/i.test(node.name.text) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.text.toLowerCase())) {
2342
- const sourceExpression = node.initializer.expression
2343
- const setters = settersForNode(node, settersByFunction)
2344
- const expression = resolveReactiveJsxExpression(sourceExpression, nearestFunction(node), setters)
2345
- const usedStates = referencedStateNames(expression, setters)
2346
- const captures = captureNames(expression, expression, setters)
2347
- if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
2348
- usesBehavior = true
2349
- usesBinding = true
2350
- const compiled = descriptors.compileReactiveBinding(expression, { setters, importBindings })
2351
- return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compiled))
2352
- }
2353
- }
2354
-
2355
- if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
2356
- const setters = settersForNode(node, settersByFunction)
2357
- const event = descriptors.compileEvent(node.initializer.expression, {
2358
- owner: fallbackOwner(node),
2359
- stateOwners: activeStateOwners ?? stateOwnersForNode(node),
2360
- setters,
2361
- reducers: reducersForNode(node, reducersByFunction),
2362
- functions: functionsForNode(node),
2363
- listItem: activeKeyedBlock ? { item: activeKeyedBlock.parts.item, index: activeKeyedBlock.parts.index } : undefined,
2364
- keyedBlock: activeKeyedBlock?.block.slot,
2365
- importBindings: new Map([...importBindings, ...packageBindings])
2366
- })
2367
- if (event) {
2368
- usesBehavior = true
2369
- return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, event))
2370
- }
2371
- if (ts.isIdentifier(node.initializer.expression) && isDestructuredParameter(node.initializer.expression, nearestFunction(node))) return node
2372
- const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
2373
- throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${node.name.text} must reference a function`)
2374
- }
2375
-
2376
- return ts.visitEachChild(node, visitor, context)
2377
- }
2378
-
2379
- const transformed = ts.visitNode(sourceFile, visitor)
2380
- descriptors.finalize()
2381
- if (!usesBehavior) return transformed
2382
-
2383
- const behaviorImports = [factory.createImportSpecifier(false, factory.createIdentifier("behavior"), factory.createIdentifier("__kBehavior"))]
2384
- if (moduleIR.handlers.some(handler => handler.kind === "module-export" && handler.role === "native")) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
2385
- if (usesBinding) {
2386
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
2387
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("select"), factory.createIdentifier("__kSelect")))
2388
- }
2389
- if (usesConditional) {
2390
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("conditional"), factory.createIdentifier("__kConditional")))
2391
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("stateConditional"), factory.createIdentifier("__kStateConditional")))
2392
- }
2393
- if (usesList) {
2394
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("list"), factory.createIdentifier("__kList")))
2395
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listExpression"), factory.createIdentifier("__kListExpression")))
2396
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listField"), factory.createIdentifier("__kListField")))
2397
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
2398
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listIndex"), factory.createIdentifier("__kListIndex")))
2399
- behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listConditional"), factory.createIdentifier("__kListConditional")))
2400
- }
2401
- if (usesListItem && !usesList) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
2402
- if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
2403
- if (usesRowState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kRowUseState")))
2404
- if (usesRowRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kRowUseRef")))
2405
- if (usesComponentState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kComponentUseState")))
2406
- if (usesComponentId) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useId"), factory.createIdentifier("__kComponentUseId")))
2407
- if (usesComponentRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kComponentUseRef")))
2408
- if (usesComponentEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kComponentUseEffect")))
2409
- if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
2410
- const behaviorImport = factory.createImportDeclaration(
2411
- undefined,
2412
- factory.createImportClause(false, undefined, factory.createNamedImports(behaviorImports)),
2413
- factory.createStringLiteral("@kudzujs/core")
2414
- )
2415
- return factory.updateSourceFile(transformed, [behaviorImport, ...transformed.statements])
2416
- }
2417
- }
2418
-
2419
- function containsRenderControl(root, knownLocals) {
2420
- let found = false
2421
- const visit = node => {
2422
- if (isFunctionLike(node) && node !== root) return
2423
- if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, knownLocals)) found = true
2424
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && containsJsx(node.right)) found = true
2425
- if (!found) ts.forEachChild(node, visit)
2426
- }
2427
- visit(root)
2428
- return found
2429
- }
2430
-
2431
- function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set(), factory = ts.factory, context, importedCollectionTransforms = new Map(), calculatedCollection, staticCollection) {
2432
- const value = unwrapExpression(expression)
2433
- const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
2434
- if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
2435
- let collection = analyzeCollectionPipeline(directFrom ? value.arguments[0] : value.expression.expression, {
2436
- setters, declarations, fail, aliases, importedCollections, stateNames: new Set(setters.values()), importedCollectionTransforms, calculatedCollection, staticCollection
2437
- })
2438
- if (!collection?.state && !collection?.calculation) return undefined
2439
- if (directFrom) collection.selector.push(["from", undefined])
2440
- let callback = directFrom ? value.arguments[1] : value.arguments[0]
2441
- const parameters = collectionParameters(callback, "Keyed list map", fail)
2442
- let root = unwrapExpression(callback.body)
2443
- if (ts.isBlock(root)) {
2444
- if (!context || root.statements.length !== 2 || !ts.isVariableStatement(root.statements[0]) || (root.statements[0].declarationList.flags & ts.NodeFlags.Const) === 0 || root.statements[0].declarationList.declarations.length !== 1 || !ts.isReturnStatement(root.statements[1]) || !root.statements[1].expression) fail(root, "Block-bodied keyed list map callbacks require one computed child collection const and a final JSX return")
2445
- const declaration = root.statements[0].declarationList.declarations[0]
2446
- if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, "Computed child collections must initialize one const identifier")
2447
- const computed = analyzeCollectionPipeline(declaration.initializer, { fail, importedCollectionTransforms })
2448
- if (!computed?.ownerField || computed.parentItem !== parameters.item) fail(declaration.initializer, `Computed child collections must start from ${parameters.item}.<field>`)
2449
- const returned = root.statements[1].expression
2450
- if (identifierReferenceCount(returned, declaration.name.text) !== 1) fail(declaration.name, `Computed child collection alias "${declaration.name.text}" must be used exactly once`)
2451
- root = unwrapExpression(substituteClone(returned, new Map([[declaration.name.text, declaration.initializer]]), factory, context))
2452
- callback = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, root)
2453
- ts.setParentRecursive(callback, false)
2454
- callback.parent = value
2455
- }
2456
- const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(setters.values()), factory, value)
2457
- if (conditional) ({ callback, root, collection } = conditional)
2458
- if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
2459
- const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2460
- const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
2461
- const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
2462
- const field = keyExpression && directProperty(keyExpression, parameters.item)
2463
- const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
2464
- if (!field && !positional) fail(key ?? root, `Keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
2465
- return { ...collection, static: collection.static && (!collection.localStatic || collection.selector.length > 0), callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : field }
2466
- }
2467
-
2468
- function nestedKeyedListParts(expression, parentItem, fail) {
2469
- const value = unwrapExpression(expression)
2470
- if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map") return undefined
2471
- let collection = analyzeCollectionPipeline(value.expression.expression, { fail })
2472
- if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
2473
- let callback = value.arguments[0]
2474
- const parameters = collectionParameters(callback, "Nested keyed list map", fail)
2475
- let root = unwrapExpression(callback.body)
2476
- const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(), ts.factory, value)
2477
- if (conditional) ({ callback, root, collection } = conditional)
2478
- if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Nested keyed list map callback must return one JSX element")
2479
- const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2480
- const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
2481
- const keyExpression = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression
2482
- const keyField = keyExpression && directProperty(keyExpression, parameters.item)
2483
- const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
2484
- if (!keyField && !positional) fail(key ?? root, `Nested keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
2485
- return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
2486
- }
2487
-
2488
- function conditionalKeyedMapRoot(callback, root, parameters, collection, fail, stateNames, factory, parent) {
2489
- let condition
2490
- let rendered
2491
- if (ts.isBinaryExpression(root) && root.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && (ts.isJsxElement(unwrapExpression(root.right)) || ts.isJsxSelfClosingElement(unwrapExpression(root.right)))) {
2492
- condition = root.left
2493
- rendered = unwrapExpression(root.right)
2494
- } else if (ts.isConditionalExpression(root) && (ts.isJsxElement(unwrapExpression(root.whenTrue)) || ts.isJsxSelfClosingElement(unwrapExpression(root.whenTrue)))) {
2495
- if (unwrapExpression(root.whenFalse).kind !== ts.SyntaxKind.NullKeyword) fail(root.whenFalse, "Conditional keyed map callbacks require condition ? <Element> : null")
2496
- condition = root.condition
2497
- rendered = unwrapExpression(root.whenTrue)
2498
- } else {
2499
- return undefined
2500
- }
2501
- if (parameters.index) fail(callback.parameters[1], "Conditional keyed map callbacks cannot use a map index because filtering changes index semantics; use an explicit filter(...).map((item, index) => ...) when a filtered index is intended")
2502
- const selectorStates = new Set(collection.selectorStates)
2503
- const selector = collectionExpression(condition, { parameters, fail, stateNames, selectorStates })
2504
- const normalized = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, rendered)
2505
- ts.setParentRecursive(normalized, false)
2506
- normalized.parent = parent
2507
- return { callback: normalized, root: rendered, collection: { ...collection, selector: [...collection.selector, ["filter", selector]], selectorStates } }
2508
- }
2509
-
2510
- function jsonExpression(value, factory) {
2511
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("JSON"), "parse"), undefined, [factory.createStringLiteral(JSON.stringify(value))])
2512
- }
2513
-
2514
- function isStateBackedListComponentCall(call, component, setters) {
2515
- if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) return false
2516
- const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
2517
- const stateNames = new Set(setters.values())
2518
- const mappedProps = new Set()
2519
- for (const element of component.parameters[0].name.elements) {
2520
- if (!ts.isIdentifier(element.name)) continue
2521
- const prop = (element.propertyName ?? element.name).getText()
2522
- const attribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.getText() === prop)
2523
- const value = attribute?.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
2524
- if (value && ts.isIdentifier(value) && stateNames.has(value.text)) mappedProps.add(element.name.text)
2525
- }
2526
- if (!mappedProps.size) return false
2527
- const returned = ts.isBlock(component.body)
2528
- ? [...component.body.statements].reverse().find(ts.isReturnStatement)?.expression
2529
- : component.body
2530
- if (!returned || !containsJsx(returned)) return false
2531
- let found = false
2532
- const visit = node => {
2533
- if (found || node !== returned && isFunctionLike(node)) return
2534
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && ts.isIdentifier(node.expression.expression) && mappedProps.has(node.expression.expression.text)) {
2535
- found = true
2536
- return
2537
- }
2538
- ts.forEachChild(node, visit)
2539
- }
2540
- visit(returned)
2541
- return found
2542
- }
2543
-
2544
- function jsxCallHasDirectStateProp(call, setters) {
2545
- const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
2546
- const stateNames = new Set(setters.values())
2547
- return attributes.properties.some(attribute => {
2548
- const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
2549
- return value && ts.isIdentifier(value) && stateNames.has(value.text)
2550
- })
2551
- }
2552
-
2553
- function jsxSetterCallbackProps(call, setters, functions, reducers) {
2554
- const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
2555
- return attributes.properties.flatMap(attribute => {
2556
- if (!ts.isJsxAttribute(attribute) || !/^on[A-Z]/.test(attribute.name.text) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return []
2557
- const value = unwrapExpression(attribute.initializer.expression)
2558
- if (ts.isIdentifier(value) && setters.has(value.text)) return [attribute.name.text]
2559
- const callback = ts.isArrowFunction(value) || ts.isFunctionExpression(value) ? value : ts.isIdentifier(value) ? functions.get(value.text) : undefined
2560
- return callback && !nativeCaptureNames(callback, setters).size && !referencedReducerDispatches(callback.body, reducers, callback).size && referencedStateNames(callback.body, setters, callback).size ? [attribute.name.text] : []
2561
- })
2562
- }
2563
-
2564
- function jsxCallHasDirectReducerProp(call, reducers) {
2565
- const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
2566
- return attributes.properties.some(attribute => {
2567
- const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
2568
- return value && ts.isIdentifier(value) && reducers.has(value.text)
2569
- })
2570
- }
2571
-
2572
- function jsxCallHasReducerCallbackProp(call, reducers) {
2573
- const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
2574
- return attributes.properties.some(attribute => {
2575
- const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
2576
- return value && referencedReducerDispatches(value, reducers, value).size
2577
- })
2578
- }
2579
-
2580
- function runtimeImportNames(sourceFile, relative) {
2581
- const names = new Set()
2582
- for (const statement of sourceFile.statements) {
2583
- if (!ts.isImportDeclaration(statement) || !statement.importClause || statement.importClause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text.startsWith(".") !== relative || isStaticImport(statement.moduleSpecifier.text)) continue
2584
- const clause = statement.importClause
2585
- if (clause.name) names.add(clause.name.text)
2586
- if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) names.add(clause.namedBindings.name.text)
2587
- if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const entry of clause.namedBindings.elements) if (!entry.isTypeOnly) names.add(entry.name.text)
2588
- }
2589
- return names
2590
- }
2591
-
2592
- function insideJsxEventHandler(node, root) {
2593
- for (let current = node.parent; current && current !== root.parent; current = current.parent) {
2594
- if (ts.isJsxAttribute(current) && /^on[A-Z]/.test(current.name.text)) return true
2595
- }
2596
- return false
2597
- }
2598
-
2599
- function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
2600
- const fail = (node, message) => {
2601
- throw sourceNodeError(node, sourceFile, message)
2602
- }
2603
- const analysis = { values: [], conditions: [], nested: [] }
2604
- const root = parts.root
2605
- const item = parts.item
2606
- const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
2607
- const validateElement = node => {
2608
- const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
2609
- if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
2610
- }
2611
- const visit = node => {
2612
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useId") fail(node, "useId() is not supported in keyed rows")
2613
- if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
2614
- if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
2615
- if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
2616
- if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
2617
- if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
2618
- return
2619
- }
2620
- if (ts.isJsxExpression(node) && node.expression) {
2621
- const expression = unwrapExpression(node.expression)
2622
- if (containsJsx(expression) && ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && expression.expression.name.text === "map") {
2623
- const nested = nestedKeyedListParts(expression, item, fail)
2624
- if (!nested) fail(expression, nestedDiagnostic)
2625
- if (["__proto__", "constructor", "prototype"].includes(nested.ownerField)) fail(expression, `Nested keyed list owner property "${nested.ownerField}" is not supported`)
2626
- if (referenceIdentifiers(nested.callback, item).length) fail(nested.root, "Nested keyed list rows cannot capture the parent item")
2627
- const specialization = componentSpecializations.get(nested.root) ?? expandedRowSpecializations.get(nested.root) ?? nestedRowSpecializations.get(`${expression.pos}:${expression.end}`)
2628
- const root = specialization?.root ?? nested.root
2629
- let callback = root === nested.root ? nested.callback : factory.updateArrowFunction(
2630
- nested.callback,
2631
- nested.callback.modifiers,
2632
- nested.callback.typeParameters,
2633
- nested.callback.parameters,
2634
- nested.callback.type,
2635
- nested.callback.equalsGreaterThanToken,
2636
- root
2637
- )
2638
- if (callback !== nested.callback) {
2639
- ts.setParentRecursive(callback, false)
2640
- callback.parent = nested.callback.parent
2641
- }
2642
- callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] })
2643
- const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
2644
- const nestedParts = {
2645
- ...nested,
2646
- root,
2647
- callback,
2648
- state: parts.state,
2649
- nested: true,
2650
- specializations: [specialization?.analysis?.slot, ...(specialization?.specializations ?? [])].filter(slot => slot !== undefined),
2651
- rowStates: specializedStates,
2652
- rowRefs: specialization?.rowRefs ?? [],
2653
- analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])])
2654
- }
2655
- for (const calculation of specialization?.calculations ?? []) {
2656
- ts.setParentRecursive(calculation, false)
2657
- calculation.parent = callback
2658
- validateListExpression(calculation, nested.item, nested.root, fail)
2659
- }
2660
- const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2661
- analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
2662
- return
2663
- }
2664
- const condition = conditionalParts(expression)
2665
- if (condition && containsJsx(expression)) {
2666
- if (rowStates.some(rowState => referencedStateNames(condition.condition, setters).has(rowState.state))) {
2667
- visit(condition.truthy)
2668
- visit(condition.falsy)
2669
- return
2670
- }
2671
- if (!referencesIdentifier(condition.condition, item) && !(parts.index && referencesIdentifier(condition.condition, parts.index))) fail(node, "Keyed list item conditions must read the item or index")
2672
- validateListExpression(condition.condition, item, node, fail, parts.index)
2673
- analysis.conditions.push({ node: node.expression, value: { ...condition, item, index: parts.index } })
2674
- visit(condition.truthy)
2675
- visit(condition.falsy)
2676
- return
2677
- }
2678
- const field = directProperty(expression, item)
2679
- const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.text === "key"
2680
- if (field && ["__proto__", "constructor", "prototype"].includes(field)) fail(node, `Keyed list item property "${field}" is not supported`)
2681
- if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
2682
- if (isRootKey) return
2683
- if (field) {
2684
- analysis.values.push({ node: node.expression, value: { field } })
2685
- return
2686
- }
2687
- if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
2688
- const states = referencedStateNames(expression, setters)
2689
- for (const rowState of rowStates) states.delete(rowState.state)
2690
- if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
2691
- validateListExpression(expression, item, node, fail, parts.index, states)
2692
- if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
2693
- analysis.values.push({ node: node.expression, value: { item, index: parts.index, states } })
2694
- return
2695
- }
2696
- }
2697
- ts.forEachChild(node, visit)
2698
- }
2699
- visit(root)
2700
- return analysis
2701
- }
2702
-
2703
- function directConstObjectLiteral(expression, call) {
2704
- expression = unwrapExpression(expression)
2705
- if (ts.isObjectLiteralExpression(expression)) return expression
2706
- if (!ts.isIdentifier(expression)) return
2707
- const scopes = []
2708
- for (let current = call.parent; current; current = current.parent) {
2709
- if (isFunctionLike(current) && ts.isBlock(current.body)) scopes.push(current.body)
2710
- if (ts.isSourceFile(current)) scopes.push(current)
2711
- }
2712
- for (const scope of scopes) {
2713
- const declarations = []
2714
- for (const statement of scope.statements) {
2715
- if (!ts.isVariableStatement(statement)) continue
2716
- for (const declaration of statement.declarationList.declarations) {
2717
- if (ts.isIdentifier(declaration.name) && declaration.name.text === expression.text) declarations.push({ declaration, constant: (statement.declarationList.flags & ts.NodeFlags.Const) !== 0 })
2718
- }
2719
- }
2720
- if (!declarations.length) continue
2721
- if (declarations.length !== 1 || !declarations[0].constant || !declarations[0].declaration.initializer || declarations[0].declaration.end >= call.pos) return
2722
- const initializer = unwrapExpression(declarations[0].declaration.initializer)
2723
- if (ts.isObjectLiteralExpression(initializer)) return initializer
2724
- return
2725
- }
2726
- }
2727
-
2728
- function specializedSpreadEntries(expression, call, fail, label, seen = new Set()) {
2729
- const object = directConstObjectLiteral(expression, call)
2730
- if (!object) fail(expression, `${label} component prop spreads must use an inline object literal or one direct const object literal declared in the calling component`)
2731
- if (seen.has(object)) fail(expression, `${label} component prop spreads cannot be circular`)
2732
- seen.add(object)
2733
- const entries = []
2734
- for (const property of object.properties) {
2735
- if (ts.isSpreadAssignment(property)) {
2736
- entries.push(...specializedSpreadEntries(property.expression, call, fail, label, seen))
2737
- continue
2738
- }
2739
- if (ts.isShorthandPropertyAssignment(property)) {
2740
- entries.push([property.name.text, property.name, property])
2741
- continue
2742
- }
2743
- if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name) && !ts.isNumericLiteral(property.name)) {
2744
- fail(property, `${label} component prop spreads must contain only direct properties`)
2745
- }
2746
- entries.push([property.name.text, property.initializer, property])
2747
- }
2748
- seen.delete(object)
2749
- return entries
2750
- }
2751
-
2752
- function specializedCallChildren(call, factory) {
2753
- if (!ts.isJsxElement(call)) return []
2754
- return call.children.flatMap(child => {
2755
- if (ts.isJsxText(child)) {
2756
- const lines = child.text.split(/\r\n|\n|\r/)
2757
- const text = lines.length === 1
2758
- ? child.text
2759
- : lines.map((line, index) => {
2760
- let text = line.replace(/\t/g, " ")
2761
- if (index) text = text.trimStart()
2762
- if (index < lines.length - 1) text = text.trimEnd()
2763
- return text
2764
- }).filter(Boolean).join(" ")
2765
- return text ? [factory.createStringLiteral(text)] : []
2766
- }
2767
- if (ts.isJsxExpression(child)) return child.expression ? [child.expression] : []
2768
- return [child]
2769
- })
2770
- }
2771
-
2772
- function flattenForwardedComponentChildren(root, factory, context) {
2773
- const forwarded = expression => {
2774
- const value = unwrapExpression(expression)
2775
- if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value)) return [value]
2776
- if (ts.isJsxFragment(value)) return [...value.children]
2777
- if (ts.isArrayLiteralExpression(value) && !value.elements.some(ts.isSpreadElement)) {
2778
- return value.elements.flatMap(element => {
2779
- if (ts.isJsxFragment(element)) return [...element.children]
2780
- if (ts.isJsxElement(element) || ts.isJsxSelfClosingElement(element)) return [element]
2781
- return [factory.createJsxExpression(undefined, element)]
2782
- })
2783
- }
2784
- }
2785
- const visit = node => {
2786
- if (ts.isJsxElement(node)) {
2787
- const children = node.children.flatMap(child => {
2788
- const values = ts.isJsxExpression(child) && child.expression ? forwarded(child.expression) : undefined
2789
- return (values ?? [child]).map(entry => ts.visitNode(entry, visit))
2790
- })
2791
- return factory.updateJsxElement(node, ts.visitNode(node.openingElement, visit), children, ts.visitNode(node.closingElement, visit))
2792
- }
2793
- return ts.visitEachChild(node, visit, context)
2794
- }
2795
- return ts.visitNode(root, visit)
2796
- }
2797
-
2798
- function expandSpecializedRest(root, returned, component, rest, entries, factory, context, fail, label) {
2799
- const sourceRoot = unwrapExpression(returned)
2800
- const sourceTag = jsxTagName(sourceRoot)
2801
- if (!sourceTag || !ts.isIdentifier(sourceTag) || sourceTag.text[0] !== sourceTag.text[0].toLowerCase()) {
2802
- fail(returned, `${label} component rest props must be forwarded exactly once to the direct intrinsic root`)
2803
- }
2804
- const sourceAttributes = ts.isJsxElement(sourceRoot) ? sourceRoot.openingElement.attributes : sourceRoot.attributes
2805
- const spreads = sourceAttributes.properties.filter(attribute => ts.isJsxSpreadAttribute(attribute) && ts.isIdentifier(unwrapExpression(attribute.expression)) && unwrapExpression(attribute.expression).text === rest.name)
2806
- const references = referenceIdentifiers(component.body, rest.name)
2807
- if (spreads.length !== 1 || references.length !== 1 || unwrapExpression(spreads[0].expression) !== references[0]) {
2808
- fail(rest.node, `${label} component rest props must be forwarded exactly once to the direct intrinsic root`)
2809
- }
2810
- for (const [name] of entries) {
2811
- if (["__proto__", "constructor", "prototype"].includes(name)) fail(rest.node, `${label} component rest prop ${JSON.stringify(name)} is not supported`)
2812
- if (name === "children") fail(rest.node, `${label} component rest props cannot forward children; destructure children explicitly`)
2813
- }
2814
- const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2815
- const expanded = attributes.properties.flatMap(attribute => {
2816
- if (!ts.isJsxSpreadAttribute(attribute) || !ts.isIdentifier(unwrapExpression(attribute.expression)) || unwrapExpression(attribute.expression).text !== rest.name) return [attribute]
2817
- return entries.map(([name, value]) => factory.createJsxAttribute(factory.createIdentifier(name), factory.createJsxExpression(undefined, cloneAst(value, factory, context))))
2818
- })
2819
- const last = new Map()
2820
- expanded.forEach((attribute, index) => {
2821
- if (ts.isJsxAttribute(attribute)) last.set(attribute.name.text, index)
2822
- })
2823
- const properties = expanded.filter((attribute, index) => !ts.isJsxAttribute(attribute) || last.get(attribute.name.text) === index)
2824
- if (ts.isJsxSelfClosingElement(root)) return factory.updateJsxSelfClosingElement(root, root.tagName, root.typeArguments, factory.updateJsxAttributes(attributes, properties))
2825
- const opening = factory.updateJsxOpeningElement(root.openingElement, root.openingElement.tagName, root.openingElement.typeArguments, factory.updateJsxAttributes(attributes, properties))
2826
- return factory.updateJsxElement(root, opening, root.children, root.closingElement)
2827
- }
2828
-
2829
- function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set()) {
2830
- if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, `${label} components must be synchronous`)
2831
- if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, `${label} components must use one destructured props parameter`)
2832
- const callAttributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
2833
- const props = new Map()
2834
- const directProps = new Set()
2835
- let key
2836
- for (const attribute of callAttributes.properties) {
2837
- if (ts.isJsxSpreadAttribute(attribute)) {
2838
- for (const [name, value, property] of specializedSpreadEntries(attribute.expression, call, fail, label)) {
2839
- if (["__proto__", "constructor", "prototype"].includes(name)) fail(property, `${label} component prop spread property ${JSON.stringify(name)} is not supported`)
2840
- if (name === "key") fail(property, `${label} component prop spreads cannot declare key`)
2841
- props.set(name, value)
2842
- }
2843
- continue
2844
- }
2845
- const name = attribute.name.text
2846
- if (directProps.has(name) || name === "key" && key) fail(attribute, `Duplicate ${label.toLowerCase()} component prop "${name}"`)
2847
- const value = !attribute.initializer
2848
- ? factory.createTrue()
2849
- : ts.isStringLiteral(attribute.initializer)
2850
- ? factory.createStringLiteral(attribute.initializer.text)
2851
- : ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression
2852
- ? attribute.initializer.expression
2853
- : factory.createIdentifier("undefined")
2854
- if (name === "key") key = attribute
2855
- else {
2856
- props.set(name, value)
2857
- directProps.add(name)
2858
- }
2859
- }
2860
- const children = specializedCallChildren(call, factory)
2861
- if (children.length) {
2862
- if (directProps.has("children")) fail(call, `Duplicate ${label.toLowerCase()} component prop "children"`)
2863
- props.set("children", children.length === 1 ? children[0] : factory.createArrayLiteralExpression(children))
2864
- }
2865
- const substitutions = new Map()
2866
- const acceptedProps = new Set()
2867
- let rest
2868
- const elements = component.parameters[0].name.elements
2869
- for (const [index, element] of elements.entries()) {
2870
- if (element.dotDotDotToken) {
2871
- if (!ts.isIdentifier(element.name) || element.propertyName || element.initializer || index !== elements.length - 1) fail(element, `${label} component rest props must be one final identifier binding`)
2872
- rest = { name: element.name.text, node: element }
2873
- continue
2874
- }
2875
- if (!ts.isIdentifier(element.name)) fail(element, `${label} component props cannot use nested destructuring`)
2876
- if (element.initializer && !isSerializableStateLiteral(element.initializer)) fail(element.initializer, `${label} component prop defaults must be directly serializable primitive, plain-object, or array literals`)
2877
- const prop = (element.propertyName ?? element.name).text
2878
- acceptedProps.add(prop)
2879
- substitutions.set(element.name.text, props.has(prop) ? props.get(prop) : element.initializer ?? factory.createIdentifier("undefined"))
2880
- }
2881
- const restEntries = [...props].filter(([prop]) => !acceptedProps.has(prop))
2882
- if (!rest) for (const [prop] of restEntries) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
2883
- const propAnalysis = elements.map(element => ({
2884
- name: (element.propertyName ?? element.name).getText(),
2885
- local: element.name.getText(),
2886
- provided: element.dotDotDotToken ? restEntries.length > 0 : props.has((element.propertyName ?? element.name).text),
2887
- ...(element.dotDotDotToken ? { rest: true } : {}),
2888
- ...(element.initializer ? { hasDefault: true, defaultApplied: !props.has((element.propertyName ?? element.name).text) } : {})
2889
- }))
2890
-
2891
- let returned
2892
- const calculations = []
2893
- const effectCalls = []
2894
- const hookDeclarations = []
2895
- const rowStates = []
2896
- const rowRefs = []
2897
- const ordinaryStates = []
2898
- const ordinaryRefs = []
2899
- const ordinaryIds = []
2900
- if (!ts.isBlock(component.body)) {
2901
- returned = component.body
2902
- } else {
2903
- const statements = [...component.body.statements]
2904
- const last = statements.pop()
2905
- if (!last || !ts.isReturnStatement(last) || !last.expression) fail(component.body, `${label} component must end with one JSX return`)
2906
- for (const statement of statements) {
2907
- if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect") {
2908
- effectCalls.push(statement.expression)
2909
- continue
2910
- }
2911
- if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.declarationList.declarations.length !== 1) fail(statement, `${label} component locals must be single const declarations`)
2912
- const declaration = statement.declarationList.declarations[0]
2913
- if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
2914
- const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
2915
- const initialArgument = declaration.initializer.arguments[0]
2916
- const propReceiver = ordinaryHooks && initialArgument && ts.isCallExpression(initialArgument) && initialArgument.arguments.length === 0 && !initialArgument.questionDotToken && ts.isPropertyAccessExpression(initialArgument.expression) && !initialArgument.expression.questionDotToken && initialArgument.expression.name.text === "toString" && ts.isIdentifier(initialArgument.expression.expression) ? initialArgument.expression.expression : undefined
2917
- const substitutedProp = propReceiver ? substitutions.get(propReceiver.text) : undefined
2918
- const propStringInitializer = substitutedProp && ts.isIdentifier(unwrapExpression(substitutedProp)) && ordinaryStateNames.has(unwrapExpression(substitutedProp).text)
2919
- if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(initialArgument) && !propStringInitializer) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useState() must use one directly serializable primitive, plain object, or array initial value${ordinaryHooks ? " or direct primitive state prop.toString()" : ""}; other dynamic initializers are not supported`)
2920
- if (!ts.isArrayBindingPattern(declaration.name) || declaration.name.elements.length !== 2 || declaration.name.elements.some(element => !element || !ts.isBindingElement(element) || !ts.isIdentifier(element.name) || element.initializer || element.dotDotDotToken)) throw sourceNodeError(declaration.name, component.getSourceFile(), `${hookLabel} useState() must use [state, setter] identifier destructuring`)
2921
- const suffix = `${Math.max(0, call.pos)}_${ordinaryHooks ? ordinaryStates.length : rowStates.length}`
2922
- const state = ordinaryHooks ? `__kComponentState${suffix}` : `__kRowState${suffix}`
2923
- const setter = ordinaryHooks ? `__kComponentSetter${suffix}` : `__kRowSetter${suffix}`
2924
- substitutions.set(declaration.name.elements[0].name.text, factory.createIdentifier(state))
2925
- substitutions.set(declaration.name.elements[1].name.text, factory.createIdentifier(setter))
2926
- const binding = factory.createArrayBindingPattern([
2927
- factory.createBindingElement(undefined, undefined, factory.createIdentifier(state)),
2928
- factory.createBindingElement(undefined, undefined, factory.createIdentifier(setter))
2929
- ])
2930
- const initialValue = propStringInitializer ? substituteClone(initialArgument, substitutions, factory, context) : cloneAst(initialArgument, factory, context)
2931
- synthesizeTree(initialValue)
2932
- const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseState" : "__kRowUseState"), undefined, [initialValue])
2933
- hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
2934
- if (ordinaryHooks) ordinaryStates.push({ state, setter, source: declaration })
2935
- else rowStates.push({ state, setter, source: declaration })
2936
- continue
2937
- }
2938
- if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
2939
- const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
2940
- if (declaration.initializer.arguments.length !== 1 || declaration.initializer.arguments[0].kind !== ts.SyntaxKind.NullKeyword) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useRef() must use the direct initial value null`)
2941
- if (!ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.name, component.getSourceFile(), `${hookLabel} useRef() must be assigned to one identifier`)
2942
- const refs = ordinaryHooks ? ordinaryRefs : rowRefs
2943
- const name = `${ordinaryHooks ? "__kComponentRef" : "__kRowRef"}${Math.max(0, call.pos)}_${refs.length}`
2944
- substitutions.set(declaration.name.text, factory.createIdentifier(name))
2945
- const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseRef" : "__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
2946
- hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2947
- refs.push({ name, source: declaration })
2948
- continue
2949
- }
2950
- if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
2951
- if (!ordinaryHooks) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "useId() is not supported in keyed row components")
2952
- if (declaration.initializer.arguments.length || !ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Setter-callback component useId() must initialize one top-level const identifier without arguments")
2953
- const name = `__kComponentId${Math.max(0, call.pos)}_${hookDeclarations.length}`
2954
- substitutions.set(declaration.name.text, factory.createIdentifier(name))
2955
- const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
2956
- hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2957
- ordinaryIds.push({ name, source: declaration })
2958
- continue
2959
- }
2960
- if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
2961
- const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
2962
- calculations.push({ name: declaration.name.text, expression: calculation })
2963
- substitutions.set(declaration.name.text, calculation)
2964
- }
2965
- returned = last.expression
2966
- }
2967
- let unsupportedHook
2968
- const findUnsupportedHook = node => {
2969
- if (unsupportedHook) return
2970
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ["useState", "useRef", "useId"].includes(node.expression.text)) unsupportedHook = node
2971
- ts.forEachChild(node, findUnsupportedHook)
2972
- }
2973
- findUnsupportedHook(returned)
2974
- for (const calculation of calculations) findUnsupportedHook(calculation.expression)
2975
- if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(), `${ordinaryHooks ? "Setter-callback component" : "Keyed row"} ${unsupportedHook.expression.text}() must be one top-level const declaration`)
2976
- let root = unwrapExpression(flattenForwardedComponentChildren(substituteClone(returned, substitutions, factory, context), factory, context))
2977
- if (rest) root = expandSpecializedRest(root, returned, component, rest, restEntries, factory, context, fail, label)
2978
- if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
2979
- const tag = jsxTagName(root)
2980
- if (!ts.isIdentifier(tag) || !allowComponentRoot && tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, `${label} component must directly return an intrinsic JSX element`)
2981
- const rootAttributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
2982
- if (rootAttributes.properties.some(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "key")) fail(root, `${label} component intrinsic root cannot declare key`)
2983
- if (key) root = addJsxAttribute(root, cloneAst(key, factory, context), factory)
2984
- ts.setParentRecursive(root, false)
2985
- root.parent = call.parent
2986
- const effects = effectCalls.map(source => ({ source, call: substituteClone(source, substitutions, factory, context) }))
2987
- return {
2988
- root,
2989
- calculations: calculations
2990
- .filter(calculation => label !== "Reducer-dispatch" || !isFunctionLike(calculation.expression) || !isEventOnlyComponentLocal(returned, calculation.name))
2991
- .map(calculation => calculation.expression),
2992
- effects,
2993
- hookDeclarations,
2994
- rowStates,
2995
- rowRefs,
2996
- ordinaryStates,
2997
- ordinaryRefs,
2998
- ordinaryIds,
2999
- propExpressions: props,
3000
- props: propAnalysis,
3001
- usesComponentId: ordinaryIds.length > 0
3002
- }
3003
- }
3004
-
3005
- function isSerializableStateLiteral(node) {
3006
- const value = unwrapExpression(node)
3007
- if (isPrimitiveDefaultLiteral(value)) return true
3008
- if (ts.isArrayLiteralExpression(value)) return value.elements.every(element => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element) && isSerializableStateLiteral(element))
3009
- if (!ts.isObjectLiteralExpression(value)) return false
3010
- return value.properties.every(property => ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && property.name.text !== "__proto__" && isSerializableStateLiteral(property.initializer))
3011
- }
3012
-
3013
- function synthesizeSerializableStateLiteral(node, factory) {
3014
- node = unwrapExpression(node)
3015
- if (ts.isStringLiteral(node)) return factory.createStringLiteral(node.text)
3016
- if (ts.isNumericLiteral(node)) return factory.createNumericLiteral(node.text)
3017
- if (node.kind === ts.SyntaxKind.TrueKeyword) return factory.createTrue()
3018
- if (node.kind === ts.SyntaxKind.FalseKeyword) return factory.createFalse()
3019
- if (node.kind === ts.SyntaxKind.NullKeyword) return factory.createNull()
3020
- if (ts.isPrefixUnaryExpression(node)) return factory.createPrefixUnaryExpression(node.operator, synthesizeSerializableStateLiteral(node.operand, factory))
3021
- if (ts.isArrayLiteralExpression(node)) return factory.createArrayLiteralExpression(node.elements.map(element => synthesizeSerializableStateLiteral(element, factory)))
3022
- return factory.createObjectLiteralExpression(node.properties.map(property => {
3023
- const name = ts.isIdentifier(property.name) ? factory.createIdentifier(property.name.text) : ts.isNumericLiteral(property.name) ? factory.createNumericLiteral(property.name.text) : factory.createStringLiteral(property.name.text)
3024
- return factory.createPropertyAssignment(name, synthesizeSerializableStateLiteral(property.initializer, factory))
3025
- }))
3026
- }
3027
-
3028
- function isPrimitiveDefaultLiteral(node) {
3029
- return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
3030
- (ts.isPrefixUnaryExpression(node) && (node.operator === ts.SyntaxKind.PlusToken || node.operator === ts.SyntaxKind.MinusToken) && ts.isNumericLiteral(node.operand)) ||
3031
- node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
3032
- }
3033
-
3034
- function isEventOnlyComponentLocal(root, name) {
3035
- let found = false
3036
- let eventOnly = true
3037
- const visit = node => {
3038
- if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) {
3039
- found = true
3040
- let parent = node.parent
3041
- while (parent && parent !== root) {
3042
- if (ts.isJsxAttribute(parent)) {
3043
- if (!/^on[A-Z]/.test(parent.name.text)) eventOnly = false
3044
- return
3045
- }
3046
- parent = parent.parent
3047
- }
3048
- eventOnly = false
3049
- return
3050
- }
3051
- ts.forEachChild(node, visit)
3052
- }
3053
- visit(root)
3054
- return found && eventOnly
3055
- }
3056
-
3057
- function substituteClone(root, substitutions, factory, context) {
3058
- const visit = (node, shadowed = new Set()) => {
3059
- if (ts.isTypeNode(node)) return cloneAst(node, factory, context)
3060
- if (ts.isShorthandPropertyAssignment(node) && substitutions.has(node.name.text) && !shadowed.has(node.name.text)) {
3061
- return factory.createPropertyAssignment(cloneAst(node.name, factory, context), cloneAst(substitutions.get(node.name.text), factory, context))
3062
- }
3063
- if (ts.isIdentifier(node) && substitutions.has(node.text) && !shadowed.has(node.text) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node)) {
3064
- return cloneAst(substitutions.get(node.text), factory, context)
3065
- }
3066
- const nextShadowed = isFunctionLike(node)
3067
- ? new Set([...shadowed, ...node.parameters.flatMap(parameter => bindingNames(parameter.name))])
3068
- : shadowed
3069
- const clone = factory.cloneNode(node)
3070
- ts.setTextRange(clone, node)
3071
- ts.setOriginalNode(clone, node)
3072
- return ts.visitEachChild(clone, child => visit(child, nextShadowed), context)
3073
- }
3074
- return visit(root)
3075
- }
3076
-
3077
- function replaceSpecializedCalls(root, replacements, context) {
3078
- const visit = node => replacements.get(node) ?? ts.visitEachChild(node, visit, context)
3079
- return ts.visitNode(root, visit)
3080
- }
3081
-
3082
- function cloneAst(root, factory, context) {
3083
- const visit = node => {
3084
- const clone = factory.cloneNode(node)
3085
- ts.setTextRange(clone, node)
3086
- ts.setOriginalNode(clone, node)
3087
- return ts.visitEachChild(clone, visit, context)
3088
- }
3089
- return visit(root)
3090
- }
3091
-
3092
- function synthesizeTree(root) {
3093
- const visit = node => {
3094
- ts.setTextRange(node, { pos: -1, end: -1 })
3095
- ts.setOriginalNode(node, undefined)
3096
- ts.forEachChild(node, visit)
3097
- }
3098
- visit(root)
3099
- return root
3100
- }
3101
-
3102
- function addJsxAttribute(root, attribute, factory) {
3103
- if (ts.isJsxSelfClosingElement(root)) {
3104
- return factory.updateJsxSelfClosingElement(root, root.tagName, root.typeArguments, factory.updateJsxAttributes(root.attributes, [attribute, ...root.attributes.properties]))
3105
- }
3106
- const opening = factory.updateJsxOpeningElement(root.openingElement, root.openingElement.tagName, root.openingElement.typeArguments, factory.updateJsxAttributes(root.openingElement.attributes, [attribute, ...root.openingElement.attributes.properties]))
3107
- return factory.updateJsxElement(root, opening, root.children, root.closingElement)
3108
- }
3109
-
3110
- function jsxTagName(node) {
3111
- return ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
3112
- }
3113
-
3114
- function isStylesheetLink(node) {
3115
- const element = ts.isJsxElement(node) ? node.openingElement : node
3116
- if (!ts.isIdentifier(element.tagName) || element.tagName.text.toLowerCase() !== "link") return false
3117
- const attribute = element.attributes.properties.find(property => ts.isJsxAttribute(property) && property.name.getText().toLowerCase() === "rel")
3118
- if (!attribute?.initializer) return false
3119
- const value = ts.isStringLiteral(attribute.initializer)
3120
- ? attribute.initializer.text
3121
- : ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression && (ts.isStringLiteral(attribute.initializer.expression) || ts.isNoSubstitutionTemplateLiteral(attribute.initializer.expression))
3122
- ? attribute.initializer.expression.text
3123
- : undefined
3124
- return value?.toLowerCase().split(/\s+/).includes("stylesheet") ?? false
3125
- }
3126
-
3127
- function isContextProviderValue(node, contexts) {
3128
- if (node.name.text !== "value") return false
3129
- const element = node.parent?.parent
3130
- const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
3131
- return ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && contexts.has(tag.expression.text)
3132
- }
3133
-
3134
- function isJsxSyntaxIdentifier(node) {
3135
- const parent = node.parent
3136
- return (ts.isJsxOpeningElement(parent) || ts.isJsxClosingElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node || ts.isJsxAttribute(parent) && parent.name === node
3137
- }
3138
-
3139
- function isDestructuredParameter(identifier, fn) {
3140
- return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
3141
- }
3142
-
3143
- function isExportedDeclaration(node) {
3144
- const statement = ts.isVariableDeclaration(node) ? node.parent?.parent : node
3145
- return statement?.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
3146
- }
3147
-
3148
- function jsxTagUses(root, name) {
3149
- const uses = []
3150
- const visit = node => {
3151
- const tag = ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
3152
- if (tag && ts.isIdentifier(tag) && tag.text === name) uses.push(node)
3153
- ts.forEachChild(node, visit)
3154
- }
3155
- visit(root)
3156
- return uses
3157
- }
3158
-
3159
- const pureListGlobals = new Set(["Boolean", "Infinity", "Math", "NaN", "Number", "String", "undefined"])
3160
- const assignmentOperators = new Set([
3161
- ts.SyntaxKind.EqualsToken, ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.MinusEqualsToken,
3162
- ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskAsteriskEqualsToken, ts.SyntaxKind.SlashEqualsToken,
3163
- ts.SyntaxKind.PercentEqualsToken, ts.SyntaxKind.LessThanLessThanEqualsToken, ts.SyntaxKind.GreaterThanGreaterThanEqualsToken,
3164
- ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, ts.SyntaxKind.AmpersandEqualsToken, ts.SyntaxKind.BarEqualsToken,
3165
- ts.SyntaxKind.CaretEqualsToken, ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken,
3166
- ts.SyntaxKind.QuestionQuestionEqualsToken
3167
- ])
3168
-
3169
- function validateListExpression(expression, item, source, fail, index, states = new Set()) {
3170
- const visit = node => {
3171
- if (ts.isTypeNode(node)) return
3172
- if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
3173
- const key = node.argumentExpression
3174
- if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(source, "Derived keyed list item computed properties require a direct string or numeric literal key")
3175
- if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(source, `Derived keyed list item property "${key.text}" is not supported`)
3176
- }
3177
- if (ts.isPropertyAccessExpression(node) && ["__proto__", "constructor", "prototype"].includes(node.name.text) || ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression) && ["__proto__", "constructor", "prototype"].includes(node.argumentExpression.text)) {
3178
- fail(source, "Derived keyed list item expressions cannot read __proto__, prototype, or constructor")
3179
- }
3180
- if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind) || ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) {
3181
- fail(source, "Derived keyed list item expressions must be pure; assignments and updates are not supported")
3182
- }
3183
- if (ts.isDeleteExpression(node) || ts.isAwaitExpression(node) || ts.isNewExpression(node) || ts.isYieldExpression(node)) {
3184
- fail(source, "Derived keyed list item expressions must be synchronous and side-effect free; delete, await, yield, and new are not supported")
3185
- }
3186
- if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isTaggedTemplateExpression(node)) {
3187
- fail(source, "Derived keyed list item expressions cannot create or invoke arbitrary functions")
3188
- }
3189
- if (ts.isCallExpression(node)) {
3190
- if (ts.isPropertyAccessExpression(node.expression)) {
3191
- const method = node.expression.name.text
3192
- if (mutatingListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call mutating method "${method}"`)
3193
- const receiver = node.expression.expression
3194
- const mathCall = ts.isIdentifier(receiver) && receiver.text === "Math" && pureMathMethods.has(method)
3195
- if (!mathCall && !pureListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call arbitrary method "${method}"`)
3196
- } else if (!ts.isIdentifier(node.expression) || !["Boolean", "Number", "String"].includes(node.expression.text)) {
3197
- fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
3198
- }
3199
- }
3200
- if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node) && node.text !== item && node.text !== index && !states.has(node.text) && !pureListGlobals.has(node.text)) {
3201
- fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
3202
- }
3203
- ts.forEachChild(node, visit)
3204
- }
3205
- visit(expression)
3206
- }
3207
-
3208
- function directProperty(expression, objectName) {
3209
- const value = unwrapExpression(expression)
3210
- if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
3211
- if (objectName !== undefined && value.expression.text !== objectName) return undefined
3212
- return value.name.text
3213
- }
3214
-
3215
- function keyedListParentTag(node) {
3216
- for (let current = node.parent; current; current = current.parent) {
3217
- if (ts.isJsxElement(current)) return current.openingElement.tagName.getText().toLowerCase()
3218
- }
3219
- return undefined
3220
- }
3221
-
3222
- function identifierReferenceCount(root, name) {
3223
- return identifierReferences(root, name).length
3224
- }
3225
-
3226
- function identifierReferences(root, name) {
3227
- const references = []
3228
- const visit = node => {
3229
- if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !ts.isJsxClosingElement(node.parent)) references.push(node)
3230
- ts.forEachChild(node, visit)
3231
- }
3232
- visit(root)
3233
- return references
3234
- }
3235
-
3236
- function isJsxLocalValue(expression, known) {
3237
- const value = unwrapExpression(expression)
3238
- if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value) || ts.isJsxFragment(value)) return true
3239
- if (ts.isIdentifier(value)) return known.has(value.text)
3240
- const parts = conditionalParts(value)
3241
- return Boolean(parts && (isJsxLocalValue(parts.truthy, known) || isJsxLocalValue(parts.falsy, known)))
3242
- }
3243
-
3244
- function conditionalParts(expression) {
3245
- const unwrap = node => ts.isParenthesizedExpression(node) ? unwrap(node.expression) : node
3246
- const value = unwrap(expression)
3247
- if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
3248
- return { kind: "and", condition: value.left, truthy: unwrap(value.right), falsy: factoryNull() }
3249
- }
3250
- if (ts.isConditionalExpression(value)) {
3251
- return { kind: "ternary", condition: value.condition, truthy: unwrap(value.whenTrue), falsy: unwrap(value.whenFalse) }
3252
- }
3253
- return undefined
3254
- }
3255
-
3256
- function factoryNull() {
3257
- return ts.factory.createNull()
3258
- }
3259
-
3260
- function settersForNode(node, settersByFunction) {
3261
- for (let current = node.parent; current; current = current.parent) {
3262
- if (!ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) continue
3263
- const setters = settersByFunction.get(current)
3264
- if (setters) return setters
3265
- }
3266
- return new Map()
3267
- }
3268
-
3269
- function reducersForNode(node, reducersByFunction) {
3270
- for (let current = node.parent; current; current = current.parent) {
3271
- if (!ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) continue
3272
- const reducers = reducersByFunction.get(current)
3273
- if (reducers) return reducers
3274
- }
3275
- return new Map()
3276
- }
3277
-
3278
- function clientImportBindings(sourceFile, file, sourceFiles) {
3279
- const bindings = new Map()
3280
- for (const node of sourceFile.statements) {
3281
- if (!ts.isImportDeclaration(node) || !node.importClause || node.importClause.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !node.moduleSpecifier.text.startsWith(".") || isStaticImport(node.moduleSpecifier.text)) continue
3282
- let target
3283
- try {
3284
- target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
3285
- } catch (error) {
3286
- throw sourceNodeError(node.moduleSpecifier, sourceFile, error.message)
3287
- }
3288
- if (node.importClause.name) bindings.set(node.importClause.name.text, { kind: "default", local: node.importClause.name.text, target })
3289
- const named = node.importClause.namedBindings
3290
- if (named && ts.isNamespaceImport(named)) bindings.set(named.name.text, { kind: "namespace", local: named.name.text, target })
3291
- if (named && ts.isNamedImports(named)) {
3292
- for (const entry of named.elements) {
3293
- if (!entry.isTypeOnly) bindings.set(entry.name.text, { kind: "named", imported: (entry.propertyName ?? entry.name).text, local: entry.name.text, target })
3294
- }
3295
- }
3296
- }
3297
- return bindings
3298
- }
3299
-
3300
- function hasFrameworkImport(sourceFile, name) {
3301
- return sourceFile.statements.some(node => {
3302
- if (!ts.isImportDeclaration(node) || node.importClause?.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !["react", "@kudzujs/core"].includes(node.moduleSpecifier.text)) return false
3303
- const bindings = node.importClause?.namedBindings
3304
- return bindings && ts.isNamedImports(bindings) && bindings.elements.some(entry => !entry.isTypeOnly && entry.name.text === name && (entry.propertyName ?? entry.name).text === name)
3305
- })
3306
- }
3307
-
3308
- function packageImportBindings(sourceFile) {
3309
- const bindings = new Map()
3310
- const rejectDynamic = node => {
3311
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
3312
- const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null
3313
- if (specifier === null) throw sourceNodeError(node, sourceFile, "Dynamic import specifiers are not supported")
3314
- if (!specifier.startsWith(".")) throw sourceNodeError(node, sourceFile, `Dynamic package import ${JSON.stringify(specifier)} is not supported`)
3315
- }
3316
- ts.forEachChild(node, rejectDynamic)
3317
- }
3318
- rejectDynamic(sourceFile)
3319
- for (const node of sourceFile.statements) {
3320
- if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue
3321
- const target = node.moduleSpecifier.text
3322
- if (!node.importClause) {
3323
- if (!target.startsWith(".") && !["react", "react-router-dom", "@kudzujs/core"].includes(target) && !target.startsWith("@kudzujs/core/")) throw sourceNodeError(node, sourceFile, `Side-effect package import ${JSON.stringify(target)} is not supported`)
3324
- continue
3325
- }
3326
- if (node.importClause.isTypeOnly) continue
3327
- if (target.startsWith(".") || target.startsWith("node:") || target === "react" || target === "react-router-dom" || target === "@kudzujs/core" || target.startsWith("@kudzujs/core/")) continue
3328
- if (node.importClause.name) bindings.set(node.importClause.name.text, { kind: "default", local: node.importClause.name.text, target, package: true })
3329
- const named = node.importClause.namedBindings
3330
- if (named && ts.isNamespaceImport(named)) bindings.set(named.name.text, { kind: "namespace", local: named.name.text, target, package: true })
3331
- if (named && ts.isNamedImports(named)) for (const entry of named.elements) if (!entry.isTypeOnly) bindings.set(entry.name.text, { kind: "named", imported: (entry.propertyName ?? entry.name).text, local: entry.name.text, target, package: true })
3332
- }
3333
- return bindings
3334
- }
3335
-
3336
- function importedSerializableCollectionNames(sourceFile, file, sourceFiles, sourceIndex) {
3337
- return new Set(importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex).keys())
3338
- }
3339
-
3340
- function importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex) {
3341
- const collections = new Map()
3342
- for (const [name, binding] of clientImportBindings(sourceFile, file, sourceFiles)) {
3343
- if (binding.kind !== "named") continue
3344
- const imported = parseSourceFile(binding.target, sourceIndex.get(binding.target))
3345
- for (const statement of imported.statements) {
3346
- if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
3347
- const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === binding.imported)
3348
- if (declaration?.initializer && ts.isArrayLiteralExpression(unwrapExpression(declaration.initializer)) && isSerializableStateLiteral(declaration.initializer)) collections.set(name, unwrapExpression(declaration.initializer))
3349
- }
3350
- }
3351
- return collections
3352
- }
3353
-
3354
- function normalizeImportedStaticCollections(sourceFile, collections, factory, context) {
3355
- if (!collections.size) return sourceFile
3356
- const visitor = node => {
3357
- if (ts.isPropertyAccessExpression(node) && node.name.text === "map" && ts.isIdentifier(node.expression) && collections.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
3358
- return factory.updatePropertyAccessExpression(node, synthesizeTree(cloneAst(collections.get(node.expression.text), factory, context)), node.name)
3359
- }
3360
- return ts.visitEachChild(node, visitor, context)
3361
- }
3362
- return ts.visitNode(sourceFile, visitor)
3363
- }
3364
-
3365
- function resolveComponentExport(file, exportName, getSource, sourceFiles, trail = []) {
3366
- const key = `${file}:${exportName}`
3367
- if (trail.includes(key)) throw new Error(`Imported keyed list component re-export cycle: ${[...trail, key].map(entry => relative(root, entry.slice(0, entry.lastIndexOf(":")))).join(" -> ")}`)
3368
- const sourceFile = getSource(file)
3369
- const nextTrail = [...trail, key]
3370
-
3371
- for (const statement of sourceFile.statements) {
3372
- if (ts.isFunctionDeclaration(statement)) {
3373
- const isDefault = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)
3374
- const isExported = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
3375
- if (exportName === "default" && isDefault || exportName !== "default" && isExported && statement.name?.text === exportName) return statement
3376
- }
3377
- if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) && exportName !== "default") {
3378
- const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === exportName)
3379
- if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
3380
- }
3381
- if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
3382
- const component = localComponentDeclaration(sourceFile, statement.expression.text)
3383
- if (component) return component
3384
- }
3385
- if (ts.isExportDeclaration(statement) && ts.isNamedExports(statement.exportClause)) {
3386
- const entry = statement.exportClause.elements.find(element => !element.isTypeOnly && element.name.text === exportName)
3387
- if (!entry) continue
3388
- const imported = (entry.propertyName ?? entry.name).text
3389
- if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
3390
- if (!statement.moduleSpecifier.text.startsWith(".")) throw sourceNodeError(statement, sourceFile, "Imported keyed list components must use relative TypeScript re-exports")
3391
- const target = resolveSourceImport(file, statement.moduleSpecifier.text, sourceFiles)
3392
- return resolveComponentExport(target, imported, getSource, sourceFiles, nextTrail)
3393
- }
3394
- const component = localComponentDeclaration(sourceFile, imported)
3395
- if (component) return component
3396
- }
3397
- }
3398
- throw new Error(`${relative(root, file)} does not export a statically analyzable keyed list component named ${JSON.stringify(exportName)}`)
3399
- }
3400
-
3401
- function localComponentDeclaration(sourceFile, name) {
3402
- for (const statement of sourceFile.statements) {
3403
- if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) return statement
3404
- if (ts.isVariableStatement(statement)) {
3405
- const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === name)
3406
- if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
3407
- }
3408
- }
3409
- return undefined
3410
- }
3411
-
3412
- async function collectClientModules(entries, sourceFiles) {
3413
- const modules = new Set()
3414
- const queue = [...new Set(entries)]
3415
- while (queue.length) {
3416
- const file = queue.shift()
3417
- if (modules.has(file)) continue
3418
- const source = await readFile(file, "utf8")
3419
- const sourceFile = parseSourceFile(file, source)
3420
- workerCompiler.rejectConstructions(sourceFile, sourceFile, "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback, not imported client helpers")
3421
- if (containsJsx(sourceFile)) throw new Error(`${relative(root, file)} Imported client helpers must not contain JSX`)
3422
- rejectUnsupportedClientImports(sourceFile, file)
3423
- modules.add(file)
3424
- for (const node of sourceFile.statements) {
3425
- if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !runtimeModuleReference(node)) continue
3426
- if (!node.moduleSpecifier.text.startsWith(".")) throw new Error(`${relative(root, file)} Imported client helpers may only use relative runtime imports`)
3427
- if (isStaticImport(node.moduleSpecifier.text)) continue
3428
- queue.push(resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles))
3429
- }
3430
- }
3431
- const outputs = new Map()
3432
- for (const file of modules) {
3433
- const output = clientModulePath(file)
3434
- if (outputs.has(output)) throw new Error(`${relative(root, file)} and ${relative(root, outputs.get(output))} emit the same client module path`)
3435
- outputs.set(output, file)
3436
- }
3437
- return [...modules].sort()
3438
- }
3439
-
3440
- async function compileClientModule(file, sourceFiles, staticFiles, importedAssets, cssModules, base) {
3441
- const source = await readFile(file, "utf8")
3442
- const transformer = context => sourceFile => {
3443
- const factory = context.factory
3444
- const visitor = node => {
3445
- if (ts.isImportDeclaration(node) && runtimeModuleReference(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
3446
- if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
3447
- const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
3448
- return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(clientModulePath(file), clientModulePath(target))), node.attributes)
3449
- }
3450
- if (ts.isExportDeclaration(node) && runtimeModuleReference(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
3451
- const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
3452
- return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(clientModulePath(file), clientModulePath(target))), node.attributes)
3453
- }
3454
- return ts.visitEachChild(node, visitor, context)
3455
- }
3456
- return ts.visitNode(sourceFile, visitor)
3457
- }
3458
- const result = ts.transpileModule(source, {
3459
- fileName: file,
3460
- compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
3461
- transformers: { before: [transformer] },
3462
- reportDiagnostics: true
3463
- })
3464
- const errors = result.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
3465
- if (errors.length) throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
3466
- return result.outputText
3467
- }
3468
-
3469
- function resolveSourceImport(importer, specifier, sourceFiles) {
3470
- const base = resolve(dirname(importer), specifier)
3471
- const extension = extname(base)
3472
- const stem = /\.(?:js|jsx|ts|tsx)$/.test(extension) ? base.slice(0, -extension.length) : base
3473
- const candidates = extension === ".ts" || extension === ".tsx"
3474
- ? [base]
3475
- : [`${stem}.ts`, `${stem}.tsx`, join(stem, "index.ts"), join(stem, "index.tsx")]
3476
- const matches = candidates.filter(candidate => sourceFiles.has(candidate))
3477
- if (matches.length !== 1) throw new Error(`${relative(root, importer)} Relative import ${JSON.stringify(specifier)} must resolve to one TypeScript file in src/`)
3478
- return matches[0]
3479
- }
3480
-
3481
- function staticImportExtension(specifier) {
3482
- return extname(specifier.split(/[?#]/, 1)[0]).toLowerCase()
3483
- }
3484
-
3485
- function isStaticImport(specifier) {
3486
- const extension = staticImportExtension(specifier)
3487
- return extension === ".css" || staticAssetExtensions.has(extension)
3488
- }
3489
-
3490
- function resolveStaticImport(importer, specifier, staticFiles) {
3491
- const target = resolve(dirname(importer), specifier.split(/[?#]/, 1)[0])
3492
- if (!staticFiles.has(target)) throw new Error(`${relative(root, importer)} Relative asset import ${JSON.stringify(specifier)} must resolve to an existing regular file under src/`)
3493
- return target
3494
- }
3495
-
3496
- async function safeStaticFiles(files) {
3497
- const sourceRoot = await realpath(sourceDirectory)
3498
- const entries = await Promise.all(files.map(async file => {
3499
- try {
3500
- const target = await realpath(file)
3501
- const path = relative(sourceRoot, target)
3502
- if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path) || !(await stat(target)).isFile()) return undefined
3503
- return file
3504
- } catch {
3505
- return undefined
3506
- }
3507
- }))
3508
- return new Set(entries.filter(Boolean))
3509
- }
3510
-
3511
- function orderSourceStyles(cssFiles, sourceFiles, sourceIndex, staticFiles) {
3512
- const ordered = []
3513
- const seenStyles = new Set()
3514
- const seenSources = new Set()
3515
- const sourceSet = new Set(sourceFiles)
3516
- const visit = file => {
3517
- if (seenSources.has(file)) return
3518
- seenSources.add(file)
3519
- const sourceFile = parseSourceFile(file, sourceIndex.get(file))
3520
- for (const statement of sourceFile.statements) {
3521
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue
3522
- const specifier = statement.moduleSpecifier.text
3523
- if (staticImportExtension(specifier) === ".css") {
3524
- let target
3525
- try { target = resolveStaticImport(file, specifier, staticFiles) } catch { continue }
3526
- if (!seenStyles.has(target)) {
3527
- seenStyles.add(target)
3528
- ordered.push(target)
3529
- }
3530
- continue
3531
- }
3532
- if (isStaticImport(specifier)) continue
3533
- try { visit(resolveSourceImport(file, specifier, sourceSet)) } catch {}
3534
- }
3535
- }
3536
- for (const file of sourceFiles.filter(file => file.startsWith(`${pagesDirectory}${sep}`) && file.endsWith(".tsx"))) visit(file)
3537
- for (const file of sourceFiles) visit(file)
3538
- return [...ordered, ...cssFiles.filter(file => !seenStyles.has(file))]
3539
- }
3540
-
3541
- async function prepareSourceStyles(cssFiles, staticFiles, importedAssets, base) {
3542
- const cssModules = new Map()
3543
- const cssOutputs = new Map()
3544
- for (const file of cssFiles) {
3545
- let css = rewriteCssUrls(await readFile(file, "utf8"), file, staticFiles, importedAssets, base)
3546
- if (file.toLowerCase().endsWith(".module.css")) {
3547
- if (/\bcomposes\s*:/i.test(maskCssCommentsAndStrings(css))) throw new Error(`${relative(root, file)} CSS Modules composes is not supported`)
3548
- const prefix = `k${createHash("sha256").update(relative(sourceDirectory, file).replaceAll(sep, "/")).digest("hex").slice(0, 8)}`
3549
- css = (await transform(css, { loader: "local-css", sourcefile: `${prefix}.css`, target: "es2022" })).code
3550
- const classes = {}
3551
- for (const match of css.matchAll(new RegExp(`\\.${prefix}_([_a-zA-Z][_a-zA-Z0-9-]*)`, "g"))) classes[match[1]] = match[0].slice(1)
3552
- cssModules.set(file, classes)
3553
- }
3554
- cssOutputs.set(file, css)
3555
- }
3556
- return { cssModules, cssOutputs }
3557
- }
3558
-
3559
- function rewriteCssUrls(css, file, staticFiles, importedAssets, base) {
3560
- let output = ""
3561
- let cursor = 0
3562
- let index = 0
3563
- while (index < css.length) {
3564
- if (css.startsWith("/*", index)) {
3565
- index = css.indexOf("*/", index + 2)
3566
- index = index === -1 ? css.length : index + 2
3567
- continue
3568
- }
3569
- if (css[index] === '"' || css[index] === "'") {
3570
- index = cssStringEnd(css, index)
3571
- continue
3572
- }
3573
- if (css.slice(index, index + 3).toLowerCase() !== "url" || /[-_a-z\d]/i.test(css[index - 1] ?? "")) {
3574
- index++
3575
- continue
3576
- }
3577
- let open = index + 3
3578
- while (/\s/.test(css[open] ?? "")) open++
3579
- if (css[open] !== "(") {
3580
- index++
3581
- continue
3582
- }
3583
- let start = open + 1
3584
- while (/\s/.test(css[start] ?? "")) start++
3585
- const quote = css[start] === '"' || css[start] === "'" ? css[start] : ""
3586
- const valueStart = quote ? start + 1 : start
3587
- let end = valueStart
3588
- if (quote) {
3589
- end = cssStringEnd(css, start) - 1
3590
- if (css[end] !== quote) {
3591
- index = open + 1
3592
- continue
3593
- }
3594
- } else {
3595
- while (end < css.length && css[end] !== ")") end += css[end] === "\\" ? 2 : 1
3596
- }
3597
- let close = quote ? end + 1 : end
3598
- while (/\s/.test(css[close] ?? "")) close++
3599
- if (css[close] !== ")") {
3600
- index = open + 1
3601
- continue
3602
- }
3603
- const value = css.slice(valueStart, end).trim()
3604
- const replacement = rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base)
3605
- output += css.slice(cursor, index) + (replacement ?? css.slice(index, close + 1))
3606
- cursor = close + 1
3607
- index = close + 1
3608
- }
3609
- return output + css.slice(cursor)
3610
- }
3611
-
3612
- function rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base) {
3613
- if (!value || value.startsWith("/") || value.startsWith("#") || value.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(value)) return undefined
3614
- const split = value.search(/[?#]/)
3615
- const pathname = split === -1 ? value : value.slice(0, split)
3616
- const suffix = split === -1 ? "" : value.slice(split)
3617
- const target = resolve(dirname(file), pathname)
3618
- if (!staticFiles.has(target)) throw new Error(`${relative(root, file)} CSS URL ${JSON.stringify(value)} must resolve to an existing regular file under src/`)
3619
- importedAssets.add(target)
3620
- const url = assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`)
3621
- return `url(${quote || '"'}${url}${suffix}${quote || '"'})`
3622
- }
3623
-
3624
- function cssStringEnd(css, start) {
3625
- const quote = css[start]
3626
- let index = start + 1
3627
- while (index < css.length) {
3628
- if (css[index] === "\\") index += 2
3629
- else if (css[index++] === quote) break
3630
- else if (css[index - 1] === "\n") break
3631
- }
3632
- return index
3633
- }
3634
-
3635
- function maskCssCommentsAndStrings(css) {
3636
- const masked = [...css]
3637
- let index = 0
3638
- while (index < css.length) {
3639
- let end
3640
- if (css.startsWith("/*", index)) {
3641
- const close = css.indexOf("*/", index + 2)
3642
- end = close === -1 ? css.length : close + 2
3643
- } else if (css[index] === '"' || css[index] === "'") {
3644
- end = cssStringEnd(css, index)
3645
- } else {
3646
- index++
3647
- continue
3648
- }
3649
- for (; index < end; index++) if (masked[index] !== "\n") masked[index] = " "
3650
- }
3651
- return masked.join("")
3652
- }
3653
-
3654
- function staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory) {
3655
- const specifier = node.moduleSpecifier.text
3656
- if (specifier.includes("\\") || specifier.includes("#")) throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports require forward-slash paths without hash suffixes")
3657
- const queryIndex = specifier.indexOf("?")
3658
- const query = queryIndex === -1 ? "" : specifier.slice(queryIndex + 1)
3659
- if (query && query !== "url") throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports support only the ?url query")
3660
- let target
3661
- try {
3662
- target = resolveStaticImport(file, specifier, staticFiles)
3663
- } catch (error) {
3664
- throw sourceNodeError(node.moduleSpecifier, sourceFile, error.message)
3665
- }
3666
- if (node.attributes) throw sourceNodeError(node.attributes, sourceFile, "Static asset import attributes are not supported")
3667
- const extension = staticImportExtension(specifier)
3668
- if (query === "url") {
3669
- if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
3670
- if (extension !== ".css") importedAssets.add(target)
3671
- const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
3672
- return staticImportReplacement(node.importClause.name.text, value, factory)
3673
- }
3674
- if (extension === ".css") {
3675
- const classes = cssModules.get(target)
3676
- if (!node.importClause) return undefined
3677
- if (!classes || !node.importClause.name || node.importClause.isTypeOnly || node.importClause.namedBindings) {
3678
- const message = classes ? "CSS Modules require one default import" : "CSS imports must be side-effect imports"
3679
- throw sourceNodeError(node.importClause, sourceFile, message)
3680
- }
3681
- const value = factory.createObjectLiteralExpression(Object.entries(classes).sort(([left], [right]) => left.localeCompare(right)).map(([name, scoped]) => factory.createPropertyAssignment(factory.createStringLiteral(name), factory.createStringLiteral(scoped))))
3682
- return staticImportReplacement(node.importClause.name.text, value, factory)
3683
- }
3684
- if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
3685
- importedAssets.add(target)
3686
- const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
3687
- return staticImportReplacement(node.importClause.name.text, value, factory)
3688
- }
3689
-
3690
- function staticImportReplacement(name, value, factory) {
3691
- return {
3692
- name,
3693
- value,
3694
- replacement: factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
3695
- factory.createVariableDeclaration(name, undefined, undefined, value)
3696
- ], ts.NodeFlags.Const))
3697
- }
3698
- }
3699
-
3700
- function runtimeModuleReference(node) {
3701
- if (ts.isExportDeclaration(node)) return !node.isTypeOnly && (!node.exportClause || !ts.isNamedExports(node.exportClause) || node.exportClause.elements.some(entry => !entry.isTypeOnly))
3702
- const clause = node.importClause
3703
- if (!clause) return true
3704
- if (clause.isTypeOnly) return false
3705
- if (clause.name || clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) return true
3706
- return clause.namedBindings?.elements.some(entry => !entry.isTypeOnly) ?? false
3707
- }
3708
-
3709
- function rejectUnsupportedClientImports(sourceFile, file) {
3710
- const visit = node => {
3711
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) throw new Error(`${relative(root, file)} Dynamic imports are not supported in imported client helpers`)
3712
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require") throw new Error(`${relative(root, file)} require() is not supported in imported client helpers`)
3713
- ts.forEachChild(node, visit)
3714
- }
3715
- visit(sourceFile)
3716
- }
3717
-
3718
- function parseSourceFile(file, source) {
3719
- return ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
3720
- }
3721
-
3722
- function layoutExportError(file, source) {
3723
- const sourceFile = parseSourceFile(file, source)
3724
- for (const statement of sourceFile.statements) {
3725
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
3726
- const specifier = statement.exportClause.elements.find(entry => entry.name.text === "layout")
3727
- if (specifier) return sourceNodeError(specifier, sourceFile, "layout export must be a function")
3728
- }
3729
- if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
3730
- const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === "layout")
3731
- if (declaration) return sourceNodeError(declaration, sourceFile, "layout export must be a function")
3732
- }
3733
- if (ts.isFunctionDeclaration(statement) && statement.name?.text === "layout") return sourceNodeError(statement, sourceFile, "layout export must be a function")
3734
- }
3735
- return new Error(`${relative(root, file)} layout export must be a function`)
3736
- }
3737
-
3738
- function clientModulePath(file) {
3739
- return `modules/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
3740
- }
3741
-
3742
- function relativeModulePath(from, to) {
3743
- const path = relative(dirname(from), to).replaceAll(sep, "/")
3744
- return path.startsWith(".") ? path : `./${path}`
3745
- }
3746
-
3747
- function compiledPath(file) {
3748
- return join(workDirectory, relative(sourceDirectory, file)).replace(/\.(?:ts|tsx)$/, ".mjs")
3749
- }
3750
-
3751
- async function loadConfig() {
3752
- for (const name of ["kudzu.config.mjs", "kudzu.config.js"]) {
3753
- const file = join(root, name)
3754
- if (!(await exists(file))) continue
3755
- const config = (await import(`${pathToFileURL(file).href}?v=${Date.now()}-${randomUUID()}`)).default ?? {}
3756
- if (!isPlainRecord(config)) throw new Error(`${name} must export a default object`)
3757
- return config
3758
- }
3759
- return {}
3760
- }
3761
-
3762
- function normalizeStyles(value, base) {
3763
- if (value === undefined) return { urls: [], sources: [] }
3764
- if (!Array.isArray(value)) throw new Error("kudzu.config styles must be an array")
3765
- const urls = []
3766
- const sources = []
3767
- for (let index = 0; index < value.length; index++) {
3768
- const style = value[index]
3769
- const label = `kudzu.config styles[${index}]`
3770
- if (typeof style === "string") {
3771
- if (!style) throw new Error(`${label} must be a non-empty URL`)
3772
- if (style.startsWith("//")) throw new Error(`${label} must be root-relative or an absolute HTTP URL`)
3773
- if (style.startsWith("/")) {
3774
- urls.push(withBase(base, style))
3775
- continue
3776
- }
3777
- if (!/^https?:\/\//i.test(style)) throw new Error(`${label} must be root-relative or an absolute HTTP URL`)
3778
- try { new URL(style) } catch { throw new Error(`${label} must be root-relative or an absolute HTTP URL`) }
3779
- urls.push(style)
3780
- continue
3781
- }
3782
- if (!isPlainRecord(style) || Object.keys(style).some(key => !["source", "output", "transform"].includes(key))) throw new Error(`${label} must be a URL or a source style object`)
3783
- if (typeof style.source !== "string" || !style.source) throw new Error(`${label}.source must be a non-empty file path`)
3784
- if (typeof style.output !== "string" || !style.output.startsWith("/") || style.output.startsWith("//") || /[%?#\\\0]/.test(style.output) || style.output.split("/").includes("..") || !style.output.endsWith(".css")) throw new Error(`${label}.output must be a root-relative .css path without query, hash, or traversal`)
3785
- if (style.transform !== undefined && typeof style.transform !== "function") throw new Error(`${label}.transform must be a function`)
3786
- const entry = { label, source: resolve(root, style.source), output: style.output, transform: style.transform }
3787
- sources.push(entry)
3788
- urls.push(withBase(base, style.output))
3789
- }
3790
- return { urls, sources }
3791
- }
3792
-
3793
- function normalizePublicDirectory(value) {
3794
- if (value === undefined) return join(root, "public")
3795
- if (typeof value !== "string" || !value) throw new Error("kudzu.config publicDir must be a non-empty directory path")
3796
- const directory = resolve(root, value)
3797
- if (directory === outputDirectory || directory === workDirectory) throw new Error("kudzu.config publicDir cannot be dist or .kudzu")
3798
- return directory
3799
- }
3800
-
3801
- async function resolveDocumentMetadata(value, context, label) {
3802
- if (value === undefined) return {}
3803
- const metadata = typeof value === "function" ? await value(context) : value
3804
- if (!isPlainRecord(metadata)) throw new Error(`${label} must be a plain object or a function returning one`)
3805
- return metadata
3806
- }
3807
-
3808
- export function normalizeNavigation(value) {
3809
- if (value === undefined) return []
3810
- if (!isPlainRecord(value)) throw new Error("kudzu.config navigation must be a plain object")
3811
- if (Object.keys(value).some(key => !["routes", "groups"].includes(key))) throw new Error("kudzu.config navigation only supports routes or groups")
3812
- if ((value.routes === undefined) === (value.groups === undefined)) throw new Error("kudzu.config navigation must define exactly one of routes or groups")
3813
- const inputs = value.routes === undefined ? value.groups : [{ routes: value.routes }]
3814
- if (!Array.isArray(inputs) || !inputs.length) throw new Error("kudzu.config navigation.groups must be a nonempty array")
3815
- const groups = inputs.map((group, groupIndex) => {
3816
- const label = value.routes === undefined ? `kudzu.config navigation.groups[${groupIndex}]` : "kudzu.config navigation"
3817
- if (!isPlainRecord(group)) throw new Error(`${label} must be a plain object`)
3818
- if (Object.keys(group).some(key => key !== "routes")) throw new Error(`${label} only supports routes`)
3819
- if (!Array.isArray(group.routes) || !group.routes.length) throw new Error(`${label}.routes must be a nonempty array`)
3820
- const routes = normalizeNavigationRoutes(group.routes, `${label}.routes`)
3821
- const id = createHash("sha256").update(JSON.stringify([...routes].sort())).digest("hex").slice(0, 16)
3822
- return { label, index: groupIndex, routes, routeSet: new Set(routes), id, assetName: value.routes === undefined ? `kudzu-navigation-${id}.js` : "kudzu-navigation.js" }
3823
- })
3824
- const identities = groups.flatMap(group => group.routes.map(route => [route, group.label]))
3825
- const seenRoutes = new Map()
3826
- for (const [route, label] of identities) {
3827
- if (seenRoutes.has(route)) throw new Error(`${label} route ${JSON.stringify(route)} duplicates ${seenRoutes.get(route)}`)
3828
- seenRoutes.set(route, label)
3829
- }
3830
- const seenAssets = new Map()
3831
- for (const group of groups) {
3832
- if (seenAssets.has(group.assetName)) throw new Error(`${group.label} navigation hash/asset collision with ${seenAssets.get(group.assetName)}`)
3833
- seenAssets.set(group.assetName, group.label)
3834
- }
3835
- return groups
3836
- }
3837
-
3838
- function normalizeNavigationRoutes(values, label) {
3839
- const routes = values.map((route, index) => {
3840
- if (typeof route !== "string" || !route.startsWith("/") || route.startsWith("//") || /[?#\\\0]/.test(route) || /%(?:2f|5c)/i.test(route)) throw new Error(`${label}[${index}] must be a root-relative path without query, hash, or traversal`)
3841
- let decoded
3842
- try { decoded = decodeURIComponent(route) } catch { throw new Error(`${label}[${index}] must be a root-relative path without query, hash, or traversal`) }
3843
- if (decoded.split("/").includes("..") || /[?#\\\0]/.test(decoded)) throw new Error(`${label}[${index}] must be a root-relative path without query, hash, or traversal`)
3844
- return route
3845
- })
3846
- if (new Set(routes).size !== routes.length) throw new Error(`${label} must contain unique paths`)
3847
- return routes
3848
- }
3849
-
3850
- function exactRouteSegments(route) {
3851
- return route.slice(1).split("/").map(segment => decodeURIComponent(segment))
3852
- }
627
+ function exactRouteSegments(route) {
628
+ return route.slice(1).split("/").map(segment => decodeURIComponent(segment))
629
+ }
3853
630
 
3854
631
  function rejectNavigationOverlap(groups) {
3855
632
  for (let leftIndex = 0; leftIndex < groups.length; leftIndex++) for (let rightIndex = leftIndex + 1; rightIndex < groups.length; rightIndex++) {
@@ -3866,14 +643,6 @@ function navigationDomainsOverlap(left, right) {
3866
643
  return left.segments.length === right.segments.length && left.segments.every((segment, index) => segment === null || right.segments[index] === null || segment === right.segments[index])
3867
644
  }
3868
645
 
3869
- function specializeNavigationTextDescriptors(source) {
3870
- const dynamic = source
3871
- .replace("const textDescriptors = globalThis.__KUDZU_TEXT_BINDINGS__ && typeof document !== \"undefined\" ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []", "const textDescriptors = () => globalThis.__KUDZU_TEXT_BINDINGS__ ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []")
3872
- .replace("const descriptor = textDescriptors[Number(node.data.slice(\"k-text:\".length))]", "const descriptor = textDescriptors()[Number(node.data.slice(\"k-text:\".length))]")
3873
- if (dynamic === source) throw new Error("Navigation text descriptor specialization did not match binding-runtime.js")
3874
- return dynamic
3875
- }
3876
-
3877
646
  function normalizeBase(value) {
3878
647
  if (value == null || value === "" || value === "/") return ""
3879
648
  if (typeof value !== "string" || !value.startsWith("/") || /[?#\0]/.test(value) || /%(?:2f|5c)/i.test(value)) throw new Error("kudzu.config base must be a root-relative path")
@@ -3883,35 +652,11 @@ function normalizeBase(value) {
3883
652
  return value.replace(/\/+$/, "")
3884
653
  }
3885
654
 
3886
- function browserPath(path) {
3887
- return path ? new URL(path, "http://kudzu.local").pathname : ""
3888
- }
3889
-
3890
- function assetPath(base, path) {
3891
- return `${base}/${path}`
3892
- }
3893
655
 
3894
- function withBase(base, path) {
3895
- return base ? `${base}${path}` : path
3896
- }
3897
656
 
3898
- const workerCompiler = createWorkerCompiler({
3899
- root,
3900
- sourceDirectory,
3901
- outputDirectory,
3902
- assetPath,
3903
- parseSourceFile,
3904
- resolveSourceImport,
3905
- runtimeModuleReference
3906
- })
3907
657
 
3908
658
  const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
3909
- const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
3910
- const printHandlerModule = createHandlerCodegen({
3911
- resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
3912
- })
3913
- const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
3914
- const normalizeReactRouterSyntax = createRouterPass({ withBase })
659
+ const printParamEntry = createParamCodegen({ browserPath, inlineJson, relativeModulePath })
3915
660
 
3916
661
  async function staticPathEntries(module, file) {
3917
662
  if (typeof module.getStaticPaths !== "function") return [{ params: {}, props: {} }]