Files
template-app/scripts/convex-gen.mjs
T
2026-11-14 09:56:45 +00:00

76 lines
4.1 KiB
JavaScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The Convex profile's generator entry (spec §5.4): `pnpm convex:gen`. Rebuilds the module
// library (the registry is read from the manifests, not from a snapshot) and rewrites
// convex/schema.ts and convex/generated/<module>.ts. The output is deterministic: regeneration
// on an unchanged registry is byte-identical, and the profile gate asserts it.
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"
import { buildRegistry } from "@conusai-kit/core"
import { writeConvexFiles } from "@conusai-kit/data-convex"
import { convexModules } from "../convex/lib/convexModules.mjs"
// The registry is read from the modules' built library (`pnpm run build:libs` first — the
// package.json script chains it), filtered to the modules the Convex runtime can deploy
// (convex/lib/convexModules.mjs, one exclusion map shared with the runtime).
import { appModules } from "../modules/dist/index.js"
const SPDX = "// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0\n"
// A module that declares its profiles and leaves out `convex` (knowledge, agent) is not deployed
// here: the runtime's module list (convex/lib/runtime.ts) and test/convex-profile.test.ts apply the
// same filter, so no wrapper is generated for a procedure the deployed registry does not hold.
const convexBound = appModules.filter((module) => module.profiles === undefined || module.profiles.includes("convex"))
const registry = buildRegistry([...convexModules(convexBound)])
const files = await writeConvexFiles(
registry,
new URL("../convex", import.meta.url).pathname,
{
mkdir,
writeFile,
},
{ header: SPDX },
)
const functionName = (name) => name.replace(/[^A-Za-z0-9_$]/g, "_")
const procedures = [...registry.procedures.values()]
.map(({ module, def }) => [def.name, { kind: def.kind, reference: `generated/${module}:${functionName(def.name)}` }])
.sort(([left], [right]) => left.localeCompare(right))
const procedureMap = `${SPDX}// Generated by @conusai-kit/data-convex (pnpm convex:gen). Do not edit by hand.\nexport const CONVEX_PROCEDURES = ${JSON.stringify(Object.fromEntries(procedures), null, 2)} as const\n`
await writeFile(new URL("../convex/generated/procedures.ts", import.meta.url), procedureMap)
// The scaffold inventory owns these optional wrapper paths even before first adoption.
// An inactive module exports no Convex functions. Keep a deterministic empty module so
// template:record can hash every declared file; selected modules use the real output above.
// A permanent fresh-scaffold prune removes the inventory entry, so it is not recreated.
const inventory = new Set(
(await readFile(new URL("../TEMPLATE-FILES", import.meta.url), "utf8")).split("\n").map((line) => line.trim()),
)
const optionalWrappers = [
"documents", // kit:documents
"guides", // kit:guides
"support", // kit:support
]
for (const id of optionalWrappers) {
const path = `generated/${id}.ts`
if (inventory.has(`convex/${path}`) && !files.some((file) => file.path === path)) {
const content = `${SPDX}// Generated by pnpm convex:gen. No callable procedures while this module is inactive.\nexport {}\n`
await writeFile(new URL(`../convex/${path}`, import.meta.url), content)
files.push({ path, content })
}
}
// Convex bundles every exported .ts function it finds, whether or not another file imports it.
// Leaving a wrapper for an excluded module deploys a public endpoint whose runtime registry does
// not contain its procedure. Remove stale wrappers deterministically so generation is also the
// profile's deployment allowlist.
const expected = new Set([...files.map((file) => file.path.split("/").at(-1)), "procedures.ts"])
for (const entry of await readdir(new URL("../convex/generated/", import.meta.url), { withFileTypes: true })) {
if (entry.isFile() && entry.name.endsWith(".ts") && !expected.has(entry.name)) {
await rm(new URL(`../convex/generated/${entry.name}`, import.meta.url))
}
}
console.log(
`convex:gen: ${files.length + 1} file(s) written (${files
.map((f) => f.path)
.concat("generated/procedures.ts")
.join(", ")})`,
)