@gonvex/cli 0.1.32 → 0.3.0

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.
Files changed (36) hide show
  1. package/README.md +30 -15
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +1 -1
  4. package/dist/browser.js.map +1 -1
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +274 -555
  7. package/dist/index.js.map +1 -1
  8. package/dist/manifest-types.d.ts +261 -0
  9. package/dist/manifest-types.js +3 -0
  10. package/dist/manifest-types.js.map +1 -0
  11. package/dist/module-artifact.d.ts +26 -0
  12. package/dist/module-artifact.js +1585 -0
  13. package/dist/module-artifact.js.map +1 -0
  14. package/dist/react.d.ts +2 -2
  15. package/dist/react.js +1 -1
  16. package/dist/react.js.map +1 -1
  17. package/dist/templates/vite-react/README.md +5 -2
  18. package/dist/templates/vite-react/gonvex/_build/module.js +407 -0
  19. package/dist/templates/vite-react/gonvex/_generated/api.ts +185 -3
  20. package/dist/templates/vite-react/gonvex/_generated/client.ts +1 -1
  21. package/dist/templates/vite-react/gonvex/_generated/{landlord → control-plane}/schema.ts +2 -1
  22. package/dist/templates/vite-react/gonvex/_generated/{landlord → control-plane}/tables.ts +1 -1
  23. package/dist/templates/vite-react/gonvex/_generated/manifest.json +201 -66
  24. package/dist/templates/vite-react/gonvex/_generated/module.json +120 -0
  25. package/dist/templates/vite-react/gonvex/_generated/react.ts +2 -2
  26. package/dist/templates/vite-react/gonvex/_generated/schema.ts +4 -34
  27. package/dist/templates/vite-react/gonvex/_generated/tenant/schema.ts +1 -30
  28. package/dist/templates/vite-react/gonvex/_generated/tenant/tables.ts +0 -30
  29. package/dist/templates/vite-react/gonvex/index.ts +1 -0
  30. package/dist/templates/vite-react/gonvex/messages.ts +47 -0
  31. package/dist/templates/vite-react/migrations/0001_messages.sql +9 -0
  32. package/dist/templates/vite-react/package.json +1 -0
  33. package/dist/templates/vite-react/src/App.tsx +5 -5
  34. package/package.json +4 -3
  35. package/dist/templates/vite-react/gonvex/messages.go +0 -38
  36. package/dist/templates/vite-react/gonvex/schema.go +0 -14
@@ -0,0 +1,1585 @@
1
+ // TypeScript server modules ship as a language-neutral module artifact: the
2
+ // runtime receives declarative function metadata plus a required,
3
+ // self-contained JavaScript bundle.
4
+ //
5
+ // Parsing stays regex- and scanner-based. Pulling the TypeScript compiler into
6
+ // the CLI at runtime
7
+ // would cost more than the declarative metadata this pipeline needs.
8
+ import { createHash } from "node:crypto";
9
+ import { existsSync } from "node:fs";
10
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
11
+ import { builtinModules } from "node:module";
12
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
+ import { rolldown } from "rolldown";
14
+ /** Bumped whenever the artifact layout changes; mixed into the hash. */
15
+ export const moduleArtifactGeneration = 7;
16
+ /** Deterministic ESM output when gonvex.json does not name one. */
17
+ const defaultBundlePath = join("_build", "module.js");
18
+ const moduleSourceExtensions = [".ts", ".tsx", ".mts", ".cts"];
19
+ const skippedDirectories = new Set(["_build", "_generated", "node_modules", "dist", "build"]);
20
+ const defaultEntrypoints = ["index.ts", "index.mts", "index.tsx", "main.ts", "module.ts"];
21
+ const nodeBuiltinImports = new Set(builtinModules.flatMap((name) => name.startsWith("node:") ? [name, name.slice(5)] : [name, `node:${name}`]));
22
+ // `gonvex auth add google` writes gonvex/auth.tsx for the browser; it is not a
23
+ // server module and must not be treated as an executable backend module.
24
+ const skippedSourceFiles = new Set(["auth.tsx", "auth.ts"]);
25
+ const moduleFunctionKinds = new Map([
26
+ ["query", { kind: "query", delivery: "oneShot" }],
27
+ ["internalquery", { kind: "query", internal: true, delivery: "oneShot" }],
28
+ ["livequery", { kind: "query", delivery: "live" }],
29
+ ["replicacollection", { kind: "query", delivery: "replica" }],
30
+ ["reducer", { kind: "reducer" }],
31
+ ["internalreducer", { kind: "reducer", internal: true }],
32
+ ["action", { kind: "action" }],
33
+ ]);
34
+ const kindAlternation = "query|internalQuery|liveQuery|replicaCollection|reducer|internalReducer|action";
35
+ const definitionPattern = new RegExp(`export\\s+(?:const|let|var)\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*(?::[^=;]+)?=\\s*(?:await\\s+)?(${kindAlternation})\\s*(<[^(){};]*>)?\\s*\\(`, "gi");
36
+ const registrationPattern = new RegExp(`\\b(?:app|server|gonvex)\\s*\\.\\s*(${kindAlternation})\\s*(<[^(){};]*>)?\\s*\\(`, "gi");
37
+ const visibilityDefinitionPattern = /export\s+(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=;]+)?=\s*visibility\s*\(/gi;
38
+ const visibilityRegistrationPattern = /\b(?:app|server|gonvex)\s*\.\s*visibility\s*\(/gi;
39
+ const cronDefinitionPattern = /export\s+(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=;]+)?=\s*(cron|tenantCron)\s*\(/gi;
40
+ const cronRegistrationPattern = /\b(?:app|server|gonvex)\s*\.\s*(cron|tenantCron)\s*\(/gi;
41
+ const invitationAcceptancePattern = /\binvitationAcceptance\s*\(\s*(["'`])([^"'`]+)\1\s*\)/g;
42
+ const identifierPattern = /[A-Za-z_$][A-Za-z0-9_$]*/y;
43
+ const keywordPattern = /(?:true|false|null|undefined)\b/y;
44
+ const numberPattern = /-?(?:0[xX][0-9a-fA-F_]+|\d[\d_]*(?:\.[\d_]*)?(?:[eE][+-]?\d+)?)/y;
45
+ /**
46
+ * Gonvex v2 application modules are TypeScript-only.
47
+ */
48
+ export async function detectProjectLanguage(backendDir, declared) {
49
+ const normalized = declared?.trim().toLowerCase();
50
+ if (normalized === "ts" || normalized === "typescript")
51
+ return "typescript";
52
+ if (normalized)
53
+ throw new Error(`unknown gonvex.json language ${JSON.stringify(declared)}; expected "typescript"`);
54
+ if (!existsSync(backendDir))
55
+ return "typescript";
56
+ const [goSources, moduleSources] = await Promise.all([
57
+ walkFiles(backendDir, (name) => name.endsWith(".go")),
58
+ moduleSourceFiles(backendDir),
59
+ ]);
60
+ if (goSources.length > 0) {
61
+ throw new Error("Go application modules were removed in Gonvex v2; migrate gonvex/*.go to TypeScript");
62
+ }
63
+ if (moduleSources.length === 0)
64
+ throw new Error("Gonvex backend has no TypeScript module sources");
65
+ return "typescript";
66
+ }
67
+ export async function moduleSourceFiles(backendDir) {
68
+ return walkFiles(backendDir, isModuleSourceFile);
69
+ }
70
+ export async function buildModuleArtifact(options) {
71
+ const sources = [...options.files].sort();
72
+ const entrypoint = resolveEntrypoint(options.root, options.backendDir, sources, options.entrypoint);
73
+ const javascript = await bundleModuleJavaScript(options, entrypoint.absolute);
74
+ const files = {};
75
+ const functions = {};
76
+ const visibilityPlans = {};
77
+ const crons = [];
78
+ let invitationAcceptanceReducer = "";
79
+ for (const file of sources) {
80
+ const contents = await readFile(file);
81
+ files[projectPath(options.root, file)] = contents.toString("base64");
82
+ for (const [path, entry] of parseModuleFunctions(options.root, options.backendDir, file, contents.toString("utf8"))) {
83
+ if (path === "control" || path.startsWith("control.")) {
84
+ throw new Error(`module function path ${JSON.stringify(path)} uses the host-reserved Control Plane namespace`);
85
+ }
86
+ if (functions[path])
87
+ throw new Error(`duplicate module function path ${JSON.stringify(path)}`);
88
+ functions[path] = entry;
89
+ }
90
+ for (const plan of parseVisibilityDefinitions(contents.toString("utf8"))) {
91
+ if (visibilityPlans[plan.table])
92
+ throw new Error(`duplicate visibility plan for table ${JSON.stringify(plan.table)}`);
93
+ visibilityPlans[plan.table] = plan;
94
+ }
95
+ crons.push(...parseCronDefinitions(contents.toString("utf8")));
96
+ invitationAcceptancePattern.lastIndex = 0;
97
+ for (const match of contents.toString("utf8").matchAll(invitationAcceptancePattern)) {
98
+ if (invitationAcceptanceReducer && invitationAcceptanceReducer !== match[2])
99
+ throw new Error("module declares more than one invitation acceptance Reducer");
100
+ invitationAcceptanceReducer = match[2];
101
+ }
102
+ }
103
+ // Versioned SQL migrations travel with the artifact so the runtime applies
104
+ // the same schema changes as the source module.
105
+ for (const file of [...options.migrations].sort()) {
106
+ files[projectPath(options.root, file)] = (await readFile(file)).toString("base64");
107
+ }
108
+ const cronNames = new Set();
109
+ for (const cron of crons) {
110
+ if (cronNames.has(cron.name))
111
+ throw new Error(`duplicate cron: ${cron.name}`);
112
+ cronNames.add(cron.name);
113
+ const target = functions[cron.function];
114
+ if (!target)
115
+ throw new Error(`cron ${JSON.stringify(cron.name)} targets unknown function ${JSON.stringify(cron.function)}`);
116
+ if (target.kind === "query")
117
+ throw new Error(`cron ${JSON.stringify(cron.name)} must target a reducer or action`);
118
+ }
119
+ for (const [path, definition] of Object.entries(functions)) {
120
+ for (const [name, binding] of Object.entries(definition.actionCapabilities?.tools ?? {})) {
121
+ const target = functions[binding.function];
122
+ if (!target)
123
+ throw new Error(`action ${JSON.stringify(path)} tool ${JSON.stringify(name)} targets unknown function ${JSON.stringify(binding.function)}`);
124
+ if (target.kind !== binding.kind)
125
+ throw new Error(`action ${JSON.stringify(path)} tool ${JSON.stringify(name)} kind does not match ${JSON.stringify(binding.function)}`);
126
+ if (binding.kind === "query" && (!target.internal || (target.delivery ?? "oneShot") !== "oneShot"))
127
+ throw new Error(`action ${JSON.stringify(path)} tool ${JSON.stringify(name)} must target an internal one-shot Query`);
128
+ if (binding.kind === "reducer" && target.internal)
129
+ throw new Error(`action ${JSON.stringify(path)} tool ${JSON.stringify(name)} must target a public business-intent Reducer`);
130
+ }
131
+ }
132
+ const sortedFiles = sortedRecord(files);
133
+ const sortedFunctions = sortedRecord(functions);
134
+ const sortedVisibility = sortedRecord(visibilityPlans);
135
+ const sortedCrons = crons.sort((left, right) => left.name.localeCompare(right.name));
136
+ if (invitationAcceptanceReducer) {
137
+ const target = sortedFunctions[invitationAcceptanceReducer];
138
+ if (!target || target.kind !== "reducer" || !target.internal)
139
+ throw new Error("invitationAcceptance must target an internal Reducer");
140
+ }
141
+ return {
142
+ language: "typescript",
143
+ generation: moduleArtifactGeneration,
144
+ hash: artifactHash({
145
+ entrypoint: entrypoint.projectPath,
146
+ files: sortedFiles,
147
+ functions: sortedFunctions,
148
+ visibility: sortedVisibility,
149
+ crons: sortedCrons,
150
+ javascript,
151
+ invitationAcceptanceReducer,
152
+ }),
153
+ entrypoint: entrypoint.projectPath,
154
+ functions: sortedFunctions,
155
+ visibility: sortedVisibility,
156
+ files: sortedFiles,
157
+ javascript,
158
+ ...(sortedCrons.length > 0 ? { crons: sortedCrons } : {}),
159
+ ...(invitationAcceptanceReducer ? { invitationAcceptanceReducer } : {}),
160
+ };
161
+ }
162
+ function parseCronDefinitions(source) {
163
+ const crons = [];
164
+ cronDefinitionPattern.lastIndex = 0;
165
+ let match;
166
+ while ((match = cronDefinitionPattern.exec(source)) !== null) {
167
+ const openParen = match.index + match[0].length - 1;
168
+ const call = readCallArguments(source, openParen);
169
+ cronDefinitionPattern.lastIndex = Math.max(call.end, openParen + 1);
170
+ const value = call.args[0]?.value;
171
+ const name = stringMember(value, "name");
172
+ const functionPath = stringMember(value, "function");
173
+ if (!isJsonObject(value) || !name || !functionPath) {
174
+ throw new Error(`cron export ${JSON.stringify(match[1])} must use a literal name and function`);
175
+ }
176
+ const intervalMs = numberMember(value, "intervalMs");
177
+ const expression = stringMember(value, "expression");
178
+ if ((intervalMs === undefined) === (expression === undefined)) {
179
+ throw new Error(`cron ${JSON.stringify(name)} requires exactly one intervalMs or expression`);
180
+ }
181
+ if (intervalMs !== undefined && (!Number.isSafeInteger(intervalMs) || intervalMs <= 0)) {
182
+ throw new Error(`cron ${JSON.stringify(name)} intervalMs must be a positive safe integer`);
183
+ }
184
+ if (expression !== undefined && !expression.trim()) {
185
+ throw new Error(`cron ${JSON.stringify(name)} expression must be non-empty`);
186
+ }
187
+ const args = readMember(value, "args");
188
+ crons.push({
189
+ name,
190
+ function: functionPath,
191
+ scope: match[2] === "tenantCron" ? "tenant" : "project",
192
+ ...(args === undefined ? {} : { args }),
193
+ ...(intervalMs === undefined ? { expression } : { intervalMs }),
194
+ });
195
+ }
196
+ // ModuleBuilder is the documented registration form (`app.cron(...)` and
197
+ // `app.tenantCron(...)`). Keep these declarations in the language-neutral
198
+ // artifact just like the exported helper form above.
199
+ cronRegistrationPattern.lastIndex = 0;
200
+ while ((match = cronRegistrationPattern.exec(source)) !== null) {
201
+ const openParen = match.index + match[0].length - 1;
202
+ const call = readCallArguments(source, openParen);
203
+ cronRegistrationPattern.lastIndex = Math.max(call.end, openParen + 1);
204
+ const value = call.args[0]?.value;
205
+ const name = stringMember(value, "name");
206
+ const functionPath = stringMember(value, "function");
207
+ if (!isJsonObject(value) || !name || !functionPath) {
208
+ throw new Error(`cron registration must use a literal name and function`);
209
+ }
210
+ const intervalMs = numberMember(value, "intervalMs");
211
+ const expression = stringMember(value, "expression");
212
+ if ((intervalMs === undefined) === (expression === undefined)) {
213
+ throw new Error(`cron ${JSON.stringify(name)} requires exactly one intervalMs or expression`);
214
+ }
215
+ if (intervalMs !== undefined && (!Number.isSafeInteger(intervalMs) || intervalMs <= 0)) {
216
+ throw new Error(`cron ${JSON.stringify(name)} intervalMs must be a positive safe integer`);
217
+ }
218
+ if (expression !== undefined && !expression.trim()) {
219
+ throw new Error(`cron ${JSON.stringify(name)} expression must be non-empty`);
220
+ }
221
+ const args = readMember(value, "args");
222
+ crons.push({
223
+ name,
224
+ function: functionPath,
225
+ scope: match[1] === "tenantCron" ? "tenant" : "project",
226
+ ...(args === undefined ? {} : { args }),
227
+ ...(intervalMs === undefined ? { expression } : { intervalMs }),
228
+ });
229
+ }
230
+ return crons;
231
+ }
232
+ function parseVisibilityDefinitions(source) {
233
+ const plans = [];
234
+ visibilityDefinitionPattern.lastIndex = 0;
235
+ let match;
236
+ while ((match = visibilityDefinitionPattern.exec(source)) !== null) {
237
+ const openParen = match.index + match[0].length - 1;
238
+ const call = readCallArguments(source, openParen);
239
+ visibilityDefinitionPattern.lastIndex = Math.max(call.end, openParen + 1);
240
+ const value = call.args[0]?.value;
241
+ const plan = parseVisibilityPlan(value);
242
+ if (!plan)
243
+ throw new Error(`visibility export ${JSON.stringify(match[1])} must use a literal visibility plan`);
244
+ plans.push(plan);
245
+ }
246
+ visibilityRegistrationPattern.lastIndex = 0;
247
+ while ((match = visibilityRegistrationPattern.exec(source)) !== null) {
248
+ const openParen = match.index + match[0].length - 1;
249
+ const call = readCallArguments(source, openParen);
250
+ visibilityRegistrationPattern.lastIndex = Math.max(call.end, openParen + 1);
251
+ const plan = parseVisibilityPlan(call.args[0]?.value);
252
+ if (!plan)
253
+ throw new Error("module visibility registration must use a literal visibility plan");
254
+ plans.push(plan);
255
+ }
256
+ return plans;
257
+ }
258
+ function parseVisibilityPlan(value) {
259
+ const table = stringMember(value, "table");
260
+ const key = stringMember(value, "key");
261
+ const rawSets = readMember(value, "sets");
262
+ const where = parseVisibilityExpression(readMember(value, "where"));
263
+ if (!table || !key || !isJsonObject(rawSets) || !where)
264
+ return undefined;
265
+ const sets = {};
266
+ for (const name of Object.keys(rawSets).sort()) {
267
+ const candidate = rawSets[name];
268
+ const setTable = stringMember(candidate, "table");
269
+ const select = stringMember(candidate, "select");
270
+ const rawJoins = readMember(candidate, "joins");
271
+ const rawWhere = readMember(candidate, "where");
272
+ if (!setTable || !select || !Array.isArray(rawJoins) || !Array.isArray(rawWhere))
273
+ return undefined;
274
+ const joins = rawJoins.map((join) => ({
275
+ table: stringMember(join, "table") ?? "",
276
+ leftColumn: stringMember(join, "leftColumn") ?? "",
277
+ rightColumn: stringMember(join, "rightColumn") ?? "",
278
+ }));
279
+ const constraints = rawWhere.map((constraint) => ({
280
+ table: stringMember(constraint, "table") ?? "",
281
+ column: stringMember(constraint, "column") ?? "",
282
+ context: stringMember(constraint, "context"),
283
+ }));
284
+ if (joins.some((join) => !join.table || !join.leftColumn || !join.rightColumn) ||
285
+ constraints.some((constraint) => !constraint.table || !constraint.column || !["account.id", "member.id", "tenant.id"].includes(constraint.context))) {
286
+ return undefined;
287
+ }
288
+ sets[name] = { table: setTable, select, joins, where: constraints };
289
+ }
290
+ if (!visibilityExpressionSetsExist(where, sets))
291
+ return undefined;
292
+ return { table, key, sets, where };
293
+ }
294
+ function parseVisibilityExpression(value) {
295
+ const operator = stringMember(value, "operator");
296
+ if (!operator || !["public", "permission", "role", "eqContext", "inSet", "and", "or", "not"].includes(operator))
297
+ return undefined;
298
+ const result = { operator: operator };
299
+ const column = stringMember(value, "column");
300
+ const context = stringMember(value, "context");
301
+ const set = stringMember(value, "set");
302
+ const expressionValue = stringMember(value, "value");
303
+ if (column)
304
+ result.column = column;
305
+ if (context === "account.id" || context === "member.id" || context === "tenant.id")
306
+ result.context = context;
307
+ if (set)
308
+ result.set = set;
309
+ if (expressionValue)
310
+ result.value = expressionValue;
311
+ const rawChildren = readMember(value, "children");
312
+ if (Array.isArray(rawChildren)) {
313
+ const children = rawChildren.map(parseVisibilityExpression);
314
+ if (children.some((child) => child === undefined))
315
+ return undefined;
316
+ result.children = children;
317
+ }
318
+ switch (result.operator) {
319
+ case "public": return Object.keys(result).length === 1 ? result : undefined;
320
+ case "permission":
321
+ case "role": return result.value ? result : undefined;
322
+ case "eqContext": return result.column && result.context ? result : undefined;
323
+ case "inSet": return result.column && result.set ? result : undefined;
324
+ case "and":
325
+ case "or": return result.children?.length ? result : undefined;
326
+ case "not": return result.children?.length === 1 ? result : undefined;
327
+ }
328
+ }
329
+ function visibilityExpressionSetsExist(expression, sets) {
330
+ if (expression.operator === "inSet" && (!expression.set || !(expression.set in sets)))
331
+ return false;
332
+ return (expression.children ?? []).every((child) => visibilityExpressionSetsExist(child, sets));
333
+ }
334
+ /** Projects the artifact functions onto the language-neutral manifest shape. */
335
+ export function moduleManifestFunctions(artifact) {
336
+ const functions = {};
337
+ for (const [path, entry] of Object.entries(artifact.functions)) {
338
+ functions[path] = {
339
+ kind: entry.kind,
340
+ handler: entry.handler,
341
+ file: entry.file,
342
+ ...(isModuleSchema(entry.args) ? { args: entry.args } : {}),
343
+ ...(isModuleSchema(entry.result) ? { result: entry.result } : {}),
344
+ ...(entry.internal ? { internal: true } : {}),
345
+ ...(entry.delivery ? { delivery: entry.delivery } : {}),
346
+ ...(entry.dependencies ? { dependencies: entry.dependencies } : {}),
347
+ ...(entry.replica ? { replica: entry.replica } : {}),
348
+ ...(entry.offline === undefined ? {} : { offline: entry.offline }),
349
+ ...(entry.optimistic === undefined ? {} : { optimistic: entry.optimistic }),
350
+ ...(entry.actionProfile === undefined ? {} : { actionProfile: entry.actionProfile }),
351
+ ...(entry.actionCapabilities === undefined ? {} : { actionCapabilities: entry.actionCapabilities }),
352
+ };
353
+ }
354
+ return functions;
355
+ }
356
+ function artifactHash(input) {
357
+ const contract = {
358
+ generation: moduleArtifactGeneration,
359
+ language: "typescript",
360
+ entrypoint: input.entrypoint,
361
+ files: input.files,
362
+ functions: input.functions,
363
+ visibility: input.visibility,
364
+ crons: input.crons,
365
+ javascript: { path: input.javascript.path, hash: input.javascript.hash },
366
+ invitationAcceptanceReducer: input.invitationAcceptanceReducer ?? "",
367
+ };
368
+ return createHash("sha256").update(canonicalJson(contract)).digest("hex");
369
+ }
370
+ function canonicalJson(value) {
371
+ if (value === null || typeof value !== "object")
372
+ return JSON.stringify(value);
373
+ if (Array.isArray(value))
374
+ return `[${value.map(canonicalJson).join(",")}]`;
375
+ return `{${Object.entries(value)
376
+ .filter(([, child]) => child !== undefined)
377
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
378
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
379
+ .join(",")}}`;
380
+ }
381
+ async function bundleModuleJavaScript(options, entrypoint) {
382
+ const buildDir = resolve(options.backendDir, "_build");
383
+ const declaredOutput = options.bundle?.trim();
384
+ if (declaredOutput && isAbsolute(declaredOutput)) {
385
+ throw new Error("gonvex.json module.bundle must be a project-relative path under gonvex/_build");
386
+ }
387
+ const outputPath = declaredOutput ? resolve(options.root, declaredOutput) : resolve(options.backendDir, defaultBundlePath);
388
+ assertInside(buildDir, outputPath, "gonvex.json module.bundle must resolve under gonvex/_build");
389
+ await mkdir(dirname(outputPath), { recursive: true });
390
+ let bundle;
391
+ try {
392
+ bundle = await rolldown({
393
+ input: entrypoint,
394
+ cwd: options.root,
395
+ platform: "neutral",
396
+ tsconfig: false,
397
+ external: () => false,
398
+ resolve: {
399
+ // The isolate implements Web APIs, not Node. Prefer packages' browser
400
+ // branches so optional Node-only helpers never enter the signed bundle.
401
+ conditionNames: ["browser", "import", "default"],
402
+ mainFields: ["browser", "module", "main"],
403
+ },
404
+ plugins: [rejectNodeBuiltinsPlugin()],
405
+ });
406
+ }
407
+ catch (error) {
408
+ throw moduleBundleError(entrypoint, error);
409
+ }
410
+ try {
411
+ const result = await bundle.generate({
412
+ file: outputPath,
413
+ format: "esm",
414
+ codeSplitting: false,
415
+ sourcemap: false,
416
+ });
417
+ const chunks = result.output.filter((item) => item.type === "chunk");
418
+ const assets = result.output.filter((item) => item.type === "asset");
419
+ if (chunks.length !== 1 || assets.length !== 0) {
420
+ throw new Error(`expected one self-contained ESM chunk, received ${chunks.length} chunks and ${assets.length} assets`);
421
+ }
422
+ const chunk = chunks[0];
423
+ const externalImports = [...chunk.imports, ...chunk.dynamicImports];
424
+ if (externalImports.length > 0) {
425
+ throw new Error(`module bundle contains unbundled imports: ${externalImports.join(", ")}`);
426
+ }
427
+ if (!chunk.code.trim())
428
+ throw new Error("module bundle is empty");
429
+ await writeFile(outputPath, chunk.code, "utf8");
430
+ const code = Buffer.from(chunk.code, "utf8");
431
+ return {
432
+ path: projectPath(options.root, outputPath),
433
+ hash: createHash("sha256").update(code).digest("hex"),
434
+ code: code.toString("base64"),
435
+ };
436
+ }
437
+ catch (error) {
438
+ throw moduleBundleError(entrypoint, error);
439
+ }
440
+ finally {
441
+ await bundle.close();
442
+ }
443
+ }
444
+ function rejectNodeBuiltinsPlugin() {
445
+ return {
446
+ name: "gonvex-reject-node-builtins",
447
+ resolveId(source, importer) {
448
+ if (!source.startsWith("node:") && !nodeBuiltinImports.has(source))
449
+ return null;
450
+ const importedBy = importer ? ` imported by ${importer}` : "";
451
+ this.error(`Node runtime module ${JSON.stringify(source)} is unavailable in Gonvex modules${importedBy}`);
452
+ },
453
+ };
454
+ }
455
+ function moduleBundleError(entrypoint, error) {
456
+ const detail = error instanceof Error ? error.message : String(error);
457
+ return new Error(`failed to bundle TypeScript module ${entrypoint}: ${detail}`, { cause: error });
458
+ }
459
+ function resolveEntrypoint(root, backendDir, sources, configured) {
460
+ const declared = configured?.trim();
461
+ if (declared && isAbsolute(declared)) {
462
+ throw new Error("gonvex.json module.entrypoint must be a project-relative path");
463
+ }
464
+ const absolute = declared ? resolve(root, declared) : defaultEntrypoints
465
+ .map((candidate) => resolve(backendDir, candidate))
466
+ .find((candidate) => sources.includes(candidate));
467
+ if (!absolute) {
468
+ throw new Error(`TypeScript modules require gonvex.json module.entrypoint or one of: ${defaultEntrypoints.map((name) => `gonvex/${name}`).join(", ")}`);
469
+ }
470
+ assertInside(root, absolute, "gonvex.json module.entrypoint must resolve inside the project");
471
+ if (!sources.includes(absolute)) {
472
+ throw new Error(`TypeScript module entrypoint ${projectPath(root, absolute)} is missing or is not a server module source`);
473
+ }
474
+ return { absolute, projectPath: projectPath(root, absolute) };
475
+ }
476
+ function assertInside(parent, candidate, message) {
477
+ const nested = relative(resolve(parent), resolve(candidate));
478
+ if (nested === ".." || nested.startsWith(`..${sep}`) || isAbsolute(nested))
479
+ throw new Error(message);
480
+ }
481
+ function parseModuleFunctions(root, backendDir, file, source) {
482
+ const relativeFile = projectPath(root, file);
483
+ const prefix = functionPathPrefix(backendDir, file);
484
+ const entries = [];
485
+ // `export const list = query({ ... })` names the function after its module
486
+ // path and exported binding, the way the generated api.ts addresses it.
487
+ definitionPattern.lastIndex = 0;
488
+ let match;
489
+ while ((match = definitionPattern.exec(source)) !== null) {
490
+ const registration = moduleFunctionKinds.get((match[2] ?? "").toLowerCase());
491
+ const openParen = match.index + match[0].length - 1;
492
+ const call = readCallArguments(source, openParen);
493
+ definitionPattern.lastIndex = Math.max(call.end, openParen + 1);
494
+ if (!registration)
495
+ continue;
496
+ const exportName = match[1];
497
+ const options = call.args.find((argument) => argument.entries)?.entries;
498
+ // `query(listMessages)` passes the handler directly instead of options.
499
+ const firstArgument = call.args[0];
500
+ const inlineHandler = firstArgument && !firstArgument.entries ? identifierText(firstArgument.text) : undefined;
501
+ const handlerEntry = options?.get("handler");
502
+ const declaredPath = stringEntry(options, "name");
503
+ entries.push([
504
+ declaredPath ?? (prefix ? `${prefix}.${exportName}` : exportName),
505
+ moduleFunction({
506
+ ...registration,
507
+ path: declaredPath ?? (prefix ? `${prefix}.${exportName}` : exportName),
508
+ file: relativeFile,
509
+ handler: identifierText(handlerEntry?.text) ?? inlineHandler ?? exportName,
510
+ exportName,
511
+ signature: handlerEntry?.text,
512
+ options,
513
+ }),
514
+ ]);
515
+ }
516
+ // The explicit registration form lets a module assign stable public paths
517
+ // independently from its exported binding names.
518
+ registrationPattern.lastIndex = 0;
519
+ while ((match = registrationPattern.exec(source)) !== null) {
520
+ const registration = moduleFunctionKinds.get((match[1] ?? "").toLowerCase());
521
+ const openParen = match.index + match[0].length - 1;
522
+ const call = readCallArguments(source, openParen);
523
+ registrationPattern.lastIndex = Math.max(call.end, openParen + 1);
524
+ if (!registration)
525
+ continue;
526
+ const declaredPath = call.args[0]?.value;
527
+ const path = typeof declaredPath === "string" ? declaredPath.trim() : "";
528
+ if (!path)
529
+ continue;
530
+ const options = call.args.find((argument) => argument.entries)?.entries;
531
+ entries.push([
532
+ path,
533
+ moduleFunction({
534
+ ...registration,
535
+ path,
536
+ file: relativeFile,
537
+ handler: identifierText(call.args[1]?.text) ?? path.split(".").pop() ?? path,
538
+ signature: options?.get("handler")?.text,
539
+ options,
540
+ }),
541
+ ]);
542
+ }
543
+ return entries;
544
+ }
545
+ function moduleFunction(input) {
546
+ const schemas = callSchemas(input.options, input.path);
547
+ const configuredDelivery = input.options?.get("delivery")?.value;
548
+ const delivery = normalizeDelivery(configuredDelivery) ?? input.delivery;
549
+ const dependencies = dependenciesFromOptions(input.options);
550
+ const replica = delivery === "replica" ? replicaFromOptions(input.options) : undefined;
551
+ if (input.internal && input.kind === "query" && delivery !== "oneShot") {
552
+ throw new Error(`internal Query ${input.path} must use one-shot delivery`);
553
+ }
554
+ if (delivery === "replica" && !replica) {
555
+ throw new Error(`Replica Collection ${input.path} requires a replica definition`);
556
+ }
557
+ if (input.kind === "query" && (delivery ?? "oneShot") === "oneShot") {
558
+ const plan = dependencies?.liveQueryPlan;
559
+ if (!plan)
560
+ throw new Error(`one-shot query ${input.path} requires a structured live query plan`);
561
+ if (!plan.table.trim() || !plan.key.trim() || !plan.columns?.length || !plan.columns.includes(plan.key)) {
562
+ throw new Error(`one-shot query ${input.path} requires a structured live query plan with a table, key, and columns including the key`);
563
+ }
564
+ }
565
+ const offline = input.options?.get("offline")?.value;
566
+ const optimistic = input.options?.get("optimistic")?.value;
567
+ const internal = input.internal || input.options?.get("internal")?.value === true;
568
+ const actionProfileValue = input.options?.get("profile")?.value;
569
+ const actionProfile = actionProfileValue === "agent" ? "agent" : "standard";
570
+ const actionCapabilities = input.options?.get("capabilities")?.value;
571
+ if (input.kind === "reducer" && optimistic !== undefined) {
572
+ validateOptimisticTransaction(optimistic);
573
+ }
574
+ if (input.kind === "action") {
575
+ if (actionProfileValue !== undefined && actionProfileValue !== "standard" && actionProfileValue !== "agent") {
576
+ throw new Error(`action ${input.path} profile must be "standard" or "agent"`);
577
+ }
578
+ validateActionCapabilities(actionProfile, actionCapabilities, input.path);
579
+ }
580
+ else if (actionProfileValue !== undefined || actionCapabilities !== undefined) {
581
+ throw new Error(`${input.kind} ${input.path} cannot declare Action capabilities`);
582
+ }
583
+ return {
584
+ kind: input.kind,
585
+ handler: input.handler,
586
+ file: input.file,
587
+ ...(internal ? { internal: true } : {}),
588
+ ...(input.exportName ? { export: input.exportName } : {}),
589
+ args: schemas.args,
590
+ result: schemas.result,
591
+ ...(dependencies ? { dependencies } : {}),
592
+ ...(delivery === undefined ? {} : { delivery }),
593
+ ...(replica ? { replica } : {}),
594
+ ...(offline === undefined ? {} : { offline }),
595
+ ...(optimistic === undefined ? {} : { optimistic }),
596
+ ...(input.kind === "action" ? { actionProfile, ...(actionCapabilities === undefined ? {} : { actionCapabilities: actionCapabilities }) } : {}),
597
+ };
598
+ }
599
+ function validateActionCapabilities(profile, value, path) {
600
+ if (value === undefined)
601
+ return;
602
+ if (!isJsonObject(value))
603
+ throw new Error(`action ${path} capabilities must be an object literal`);
604
+ const allowed = new Set(["networkOrigins", "secrets", "tools", "scheduler", "storage", "sandbox"]);
605
+ for (const field of Object.keys(value)) {
606
+ if (!allowed.has(field))
607
+ throw new Error(`action ${path} capabilities has unsupported field ${field}`);
608
+ }
609
+ const origins = value.networkOrigins;
610
+ if (origins !== undefined) {
611
+ if (!Array.isArray(origins) || origins.length === 0)
612
+ throw new Error(`action ${path} networkOrigins must be a non-empty array`);
613
+ const seen = new Set();
614
+ for (const origin of origins) {
615
+ if (typeof origin !== "string")
616
+ throw new Error(`action ${path} networkOrigins must contain strings`);
617
+ let parsed;
618
+ try {
619
+ parsed = new URL(origin);
620
+ }
621
+ catch {
622
+ throw new Error(`action ${path} network origin ${JSON.stringify(origin)} is invalid`);
623
+ }
624
+ if ((parsed.protocol !== "https:" && parsed.protocol !== "http:") || parsed.origin !== origin || parsed.username || parsed.password) {
625
+ throw new Error(`action ${path} network origin ${JSON.stringify(origin)} must be an exact HTTP(S) origin`);
626
+ }
627
+ if (seen.has(origin))
628
+ throw new Error(`action ${path} declares duplicate network origin ${origin}`);
629
+ seen.add(origin);
630
+ }
631
+ }
632
+ const secrets = value.secrets;
633
+ if (secrets !== undefined && (!Array.isArray(secrets) || secrets.some((name) => typeof name !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(name)))) {
634
+ throw new Error(`action ${path} secrets must be uppercase environment names`);
635
+ }
636
+ const tools = value.tools;
637
+ if (tools !== undefined) {
638
+ if (profile !== "agent")
639
+ throw new Error(`action ${path} tools require profile "agent"`);
640
+ if (!isJsonObject(tools) || Object.keys(tools).length === 0)
641
+ throw new Error(`agent action ${path} tools must be a non-empty object`);
642
+ for (const [name, binding] of Object.entries(tools)) {
643
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) || !isJsonObject(binding) ||
644
+ (binding.kind !== "query" && binding.kind !== "reducer") || typeof binding.function !== "string" || !binding.function.trim()) {
645
+ throw new Error(`agent action ${path} has an invalid tool binding ${JSON.stringify(name)}`);
646
+ }
647
+ }
648
+ }
649
+ if (value.scheduler !== undefined && value.scheduler !== true)
650
+ throw new Error(`action ${path} scheduler must be true when declared`);
651
+ if (value.storage !== undefined && value.storage !== true)
652
+ throw new Error(`action ${path} storage must be true when declared`);
653
+ if (value.sandbox !== undefined) {
654
+ if (profile !== "agent")
655
+ throw new Error(`action ${path} sandbox requires profile "agent"`);
656
+ if (!isJsonObject(value.sandbox))
657
+ throw new Error(`action ${path} sandbox must be an object literal`);
658
+ for (const field of Object.keys(value.sandbox)) {
659
+ if (field !== "duckdb")
660
+ throw new Error(`action ${path} sandbox has unsupported field ${field}`);
661
+ }
662
+ if (value.sandbox.duckdb !== undefined && value.sandbox.duckdb !== true) {
663
+ throw new Error(`action ${path} sandbox.duckdb must be true when declared`);
664
+ }
665
+ }
666
+ }
667
+ /**
668
+ * Validate the literal optimistic contract while producing the artifact. The
669
+ * module itself is still validated by @gonvex/module-sdk at load time, but a
670
+ * malformed literal must not be silently copied into a client manifest.
671
+ */
672
+ function validateOptimisticTransaction(value) {
673
+ if (!isJsonObject(value) || !Array.isArray(value.effects) || value.effects.length === 0) {
674
+ throw new Error("reducer optimistic metadata must contain a non-empty effects array");
675
+ }
676
+ if (value.expectedRevision !== undefined && (typeof value.expectedRevision !== "number"
677
+ || !Number.isSafeInteger(value.expectedRevision)
678
+ || value.expectedRevision < 0)) {
679
+ throw new Error("reducer optimistic expectedRevision must be a non-negative integer");
680
+ }
681
+ for (const effect of value.effects) {
682
+ if (!isJsonObject(effect) || (effect.operation !== "patch" && effect.operation !== "upsert" && effect.operation !== "delete")) {
683
+ throw new Error("reducer has an invalid optimistic effect");
684
+ }
685
+ if (typeof effect.entity !== "string" || !effect.entity.trim()) {
686
+ throw new Error("reducer optimistic effects require an entity");
687
+ }
688
+ if (typeof effect.id !== "string" && (!Array.isArray(effect.id)
689
+ || effect.id.length === 0
690
+ || effect.id.some((part) => typeof part !== "string" || !part.trim()))) {
691
+ throw new Error("reducer optimistic effects require a string id or id references");
692
+ }
693
+ if ((effect.operation === "patch" || effect.operation === "upsert") && !isJsonObject(effect.operation === "patch" ? effect.fields : effect.value)) {
694
+ throw new Error(`reducer optimistic ${effect.operation} effects require an object value`);
695
+ }
696
+ }
697
+ }
698
+ function isJsonObject(value) {
699
+ return typeof value === "object" && value !== null && !Array.isArray(value);
700
+ }
701
+ function callSchemas(options, path) {
702
+ if (!options)
703
+ throw new Error(`TypeScript function ${JSON.stringify(path)} must declare literal args and result schemas`);
704
+ return {
705
+ args: parseRequiredSchema(options, "args", path),
706
+ result: parseRequiredSchema(options, "result", path),
707
+ };
708
+ }
709
+ function parseRequiredSchema(options, field, path) {
710
+ const entry = options.get(field);
711
+ if (!entry)
712
+ throw new Error(`TypeScript function ${JSON.stringify(path)} must declare ${field}: schema.*(...)`);
713
+ const schema = parsePortableSchema(entry.text);
714
+ if (!schema)
715
+ throw new Error(`TypeScript function ${JSON.stringify(path)} ${field} must use a static schema.*(...) declaration`);
716
+ if (!portableSchemaMatchesRuntime(schema)) {
717
+ throw new Error(`TypeScript function ${JSON.stringify(path)} ${field} uses schema.optional outside an object field, which the runtime does not support`);
718
+ }
719
+ return schema;
720
+ }
721
+ /** Keep static artifacts aligned with the Rust ABI's optional-field rule. */
722
+ function portableSchemaMatchesRuntime(schema, optionalField = false) {
723
+ switch (schema.kind) {
724
+ case "optional":
725
+ return optionalField && portableSchemaMatchesRuntime(schema.value);
726
+ case "array":
727
+ return portableSchemaMatchesRuntime(schema.items);
728
+ case "record":
729
+ return portableSchemaMatchesRuntime(schema.values);
730
+ case "object":
731
+ return Object.values(schema.fields).every((field) => portableSchemaMatchesRuntime(field, field.kind === "optional"));
732
+ default:
733
+ return true;
734
+ }
735
+ }
736
+ /** Parse only the module SDK's literal schema constructors; never evaluate source. */
737
+ function parsePortableSchema(text) {
738
+ const source = text.trim();
739
+ const match = /^schema\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/.exec(source);
740
+ if (!match)
741
+ return undefined;
742
+ const openParen = source.indexOf("(", match.index + match[0].length - 1);
743
+ const call = readCallArguments(source, openParen);
744
+ if (call.end <= openParen || skipTrivia(source, call.end) !== source.length)
745
+ return undefined;
746
+ const name = match[1];
747
+ const argument = call.args[0];
748
+ switch (name) {
749
+ case "string": {
750
+ if (call.args.length > 1)
751
+ return undefined;
752
+ const options = schemaOptions(argument, ["format", "minLength", "maxLength"]);
753
+ if (options === undefined)
754
+ return argument === undefined ? { kind: "string" } : undefined;
755
+ if (options.format !== undefined && !["email", "uri", "uuid", "datetime"].includes(String(options.format)))
756
+ return undefined;
757
+ return { kind: "string", ...options };
758
+ }
759
+ case "email":
760
+ case "uri":
761
+ case "uuid":
762
+ case "datetime":
763
+ return call.args.length === 0 ? { kind: "string", format: name } : undefined;
764
+ case "number":
765
+ case "integer": {
766
+ if (call.args.length > 1)
767
+ return undefined;
768
+ const options = schemaOptions(argument, ["minimum", "maximum"]);
769
+ if (options === undefined && argument !== undefined)
770
+ return undefined;
771
+ return { kind: "number", ...(name === "integer" ? { integer: true } : {}), ...(options ?? {}) };
772
+ }
773
+ case "boolean": return call.args.length === 0 ? { kind: "boolean" } : undefined;
774
+ case "null": return call.args.length === 0 ? { kind: "null" } : undefined;
775
+ case "any": return call.args.length === 0 ? { kind: "any" } : undefined;
776
+ case "id": return call.args.length === 1 && typeof argument?.value === "string" && argument.value.trim() ? { kind: "id", entity: argument.value } : undefined;
777
+ case "literal": return call.args.length === 1 && argument?.value !== undefined ? { kind: "literal", value: argument.value } : undefined;
778
+ case "array":
779
+ return call.args.length === 1 ? schemaChild(argument) : undefined;
780
+ case "record":
781
+ return call.args.length === 1 ? schemaChild(argument, "record") : undefined;
782
+ case "optional":
783
+ return call.args.length === 1 ? schemaChild(argument, "optional") : undefined;
784
+ case "object": {
785
+ if ((call.args.length !== 1 && call.args.length !== 2) || !argument?.entries || argument.text.includes("..."))
786
+ return undefined;
787
+ const fields = {};
788
+ for (const [key, entry] of argument.entries) {
789
+ const field = parsePortableSchema(entry.text);
790
+ if (!field)
791
+ return undefined;
792
+ fields[key] = field;
793
+ }
794
+ const options = call.args.length === 2 ? schemaOptions(call.args[1], ["allowUnknown"]) : {};
795
+ if (options === undefined)
796
+ return undefined;
797
+ return { kind: "object", fields, ...(options.allowUnknown === undefined ? {} : { allowUnknown: options.allowUnknown === true }) };
798
+ }
799
+ default: return undefined;
800
+ }
801
+ }
802
+ function schemaChild(argument, wrapper) {
803
+ const child = argument && parsePortableSchema(argument.text);
804
+ if (!child)
805
+ return undefined;
806
+ if (wrapper === "record")
807
+ return { kind: "record", values: child };
808
+ if (wrapper === "optional")
809
+ return { kind: "optional", value: child };
810
+ return { kind: "array", items: child };
811
+ }
812
+ function schemaOptions(argument, allowed) {
813
+ if (!argument)
814
+ return {};
815
+ if (!argument.entries || argument.text.includes("..."))
816
+ return undefined;
817
+ const result = {};
818
+ for (const [key, entry] of argument.entries) {
819
+ if (!allowed.includes(key) || entry.value === undefined)
820
+ return undefined;
821
+ result[key] = entry.value;
822
+ }
823
+ return result;
824
+ }
825
+ /** Validate a schema after JSON transport; used by manifest projections and tests. */
826
+ export function isModuleSchema(value) {
827
+ if (!value || typeof value !== "object" || Array.isArray(value))
828
+ return false;
829
+ const record = value;
830
+ if (typeof record.kind !== "string")
831
+ return false;
832
+ switch (record.kind) {
833
+ case "string": return schemaKeys(record, ["kind", "format", "minLength", "maxLength"])
834
+ && (record.format === undefined || ["email", "uri", "uuid", "datetime"].includes(String(record.format)))
835
+ && positiveIntegerOrUndefined(record.minLength) && positiveIntegerOrUndefined(record.maxLength);
836
+ case "number": return schemaKeys(record, ["kind", "integer", "minimum", "maximum"])
837
+ && (record.integer === undefined || typeof record.integer === "boolean")
838
+ && numberOrUndefined(record.minimum) && numberOrUndefined(record.maximum);
839
+ case "boolean":
840
+ case "null":
841
+ case "any": return schemaKeys(record, ["kind"]);
842
+ case "id": return schemaKeys(record, ["kind", "entity"]) && typeof record.entity === "string" && record.entity.trim().length > 0;
843
+ case "literal": return schemaKeys(record, ["kind", "value"]) && isJsonValue(record.value);
844
+ case "array": return schemaKeys(record, ["kind", "items"]) && isModuleSchema(record.items);
845
+ case "record": return schemaKeys(record, ["kind", "values"]) && isModuleSchema(record.values);
846
+ case "optional": return schemaKeys(record, ["kind", "value"]) && isModuleSchema(record.value);
847
+ case "object": {
848
+ if (!schemaKeys(record, ["kind", "fields", "allowUnknown"]) || !record.fields || typeof record.fields !== "object" || Array.isArray(record.fields))
849
+ return false;
850
+ if (record.allowUnknown !== undefined && typeof record.allowUnknown !== "boolean")
851
+ return false;
852
+ return Object.values(record.fields).every(isModuleSchema);
853
+ }
854
+ default: return false;
855
+ }
856
+ }
857
+ function schemaKeys(record, allowed) {
858
+ return Object.keys(record).every((key) => allowed.includes(key));
859
+ }
860
+ function numberOrUndefined(value) { return value === undefined || (typeof value === "number" && Number.isFinite(value)); }
861
+ function positiveIntegerOrUndefined(value) { return value === undefined || (typeof value === "number" && Number.isSafeInteger(value) && value >= 0); }
862
+ function isJsonValue(value) {
863
+ if (value === null || typeof value === "string" || typeof value === "boolean")
864
+ return true;
865
+ if (typeof value === "number")
866
+ return Number.isFinite(value);
867
+ if (Array.isArray(value))
868
+ return value.every(isJsonValue);
869
+ return typeof value === "object" && value !== null && Object.values(value).every(isJsonValue);
870
+ }
871
+ function signatureTypes(text) {
872
+ const openParen = text.indexOf("(");
873
+ if (openParen < 0)
874
+ return {};
875
+ const closeParen = findMatching(text, openParen);
876
+ if (closeParen < 0)
877
+ return {};
878
+ // Handlers take the context first and the arguments second.
879
+ const parameter = splitTopLevel(text.slice(openParen + 1, closeParen)).at(1) ?? "";
880
+ const colon = parameter.indexOf(":");
881
+ const args = colon < 0 ? undefined : parameter.slice(colon + 1).trim();
882
+ let result;
883
+ const afterParams = skipTrivia(text, closeParen + 1);
884
+ if (text[afterParams] === ":") {
885
+ const arrow = indexOfTopLevel(text, "=>", afterParams + 1);
886
+ result = text.slice(afterParams + 1, arrow < 0 ? text.length : arrow);
887
+ }
888
+ return { args: args || undefined, result: unwrapPromise(result) };
889
+ }
890
+ function unwrapPromise(type) {
891
+ const declared = type?.trim();
892
+ if (!declared)
893
+ return undefined;
894
+ const match = /^Promise\s*<([\s\S]*)>$/.exec(declared);
895
+ return (match ? match[1].trim() : declared) || undefined;
896
+ }
897
+ function dependenciesFromOptions(options) {
898
+ if (!options)
899
+ return undefined;
900
+ const dependencies = {};
901
+ const liveQueryPlan = liveQueryPlanFromOptions(options);
902
+ if (liveQueryPlan) {
903
+ dependencies.liveQueryPlan = liveQueryPlan;
904
+ }
905
+ if (options.get("shareByPermissions")?.value === true)
906
+ dependencies.shareByPermissions = true;
907
+ const shareResultFrom = stringEntry(options, "shareResultFrom");
908
+ if (shareResultFrom)
909
+ dependencies.shareResultFrom = shareResultFrom;
910
+ const shareResultField = stringEntry(options, "shareResultField");
911
+ if (shareResultField)
912
+ dependencies.shareResultField = shareResultField;
913
+ const optimistic = options.get("optimistic")?.value;
914
+ const nonOptimisticReason = stringEntry(options, "nonOptimisticReason");
915
+ if (nonOptimisticReason)
916
+ dependencies.nonOptimisticReason = nonOptimisticReason;
917
+ return Object.keys(dependencies).length > 0 ? dependencies : undefined;
918
+ }
919
+ function normalizeDelivery(value) {
920
+ if (value === "oneShot" || value === "live" || value === "replica")
921
+ return value;
922
+ return undefined;
923
+ }
924
+ function liveQueryPlanFromOptions(options) {
925
+ return parseLiveQueryPlan(options.get("liveQueryPlan")?.value);
926
+ }
927
+ function parseLiveQueryPlan(value) {
928
+ const table = stringMember(value, "table");
929
+ if (!table)
930
+ return undefined;
931
+ const plan = {
932
+ table,
933
+ key: stringMember(value, "key") ?? "id",
934
+ };
935
+ const columns = stringArray(readMember(value, "columns"));
936
+ if (columns)
937
+ plan.columns = columns;
938
+ const resultPath = pathArray(readMember(value, "resultPath"));
939
+ if (resultPath.length > 0)
940
+ plan.resultPath = resultPath;
941
+ const where = parseLiveExpression(readMember(value, "where"));
942
+ if (where)
943
+ plan.where = where;
944
+ const searchValue = readMember(value, "search");
945
+ const searchArgument = stringMember(searchValue, "argument");
946
+ const searchColumns = stringArray(readMember(searchValue, "columns"));
947
+ if (searchArgument && searchColumns)
948
+ plan.search = { argument: searchArgument, columns: searchColumns };
949
+ const filtersValue = readMember(value, "filters");
950
+ const filtersArgument = stringMember(filtersValue, "argument");
951
+ const filtersColumns = stringArray(readMember(filtersValue, "allowedColumns"));
952
+ const filtersOperators = stringArray(readMember(filtersValue, "allowedOperators"));
953
+ if (filtersArgument && filtersColumns && filtersOperators) {
954
+ plan.filters = { argument: filtersArgument, allowedColumns: filtersColumns, allowedOperators: filtersOperators };
955
+ }
956
+ const sortValue = readMember(value, "sort");
957
+ const sortDefaultColumn = stringMember(sortValue, "defaultColumn");
958
+ const sortDefaultDirection = stringMember(sortValue, "defaultDirection");
959
+ const sortAllowedColumns = stringArray(readMember(sortValue, "allowedColumns"));
960
+ if (sortDefaultColumn && (sortDefaultDirection === "asc" || sortDefaultDirection === "desc") && sortAllowedColumns) {
961
+ plan.sort = {
962
+ columnArgument: stringMember(sortValue, "columnArgument"),
963
+ directionArgument: stringMember(sortValue, "directionArgument"),
964
+ defaultColumn: sortDefaultColumn,
965
+ defaultDirection: sortDefaultDirection,
966
+ allowedColumns: sortAllowedColumns,
967
+ };
968
+ }
969
+ const windowValue = readMember(value, "window");
970
+ const offsetArgument = stringMember(windowValue, "offsetArgument");
971
+ const limitArgument = stringMember(windowValue, "limitArgument");
972
+ const defaultLimit = numberMember(windowValue, "defaultLimit");
973
+ const maxLimit = numberMember(windowValue, "maxLimit");
974
+ if (offsetArgument && limitArgument && defaultLimit !== undefined && maxLimit !== undefined) {
975
+ const count = stringMember(windowValue, "count");
976
+ if (count !== undefined && count !== "exact")
977
+ throw new Error("live query window count must be exact");
978
+ plan.window = { offsetArgument, limitArgument, defaultLimit, maxLimit, ...(count ? { count: "exact" } : {}) };
979
+ }
980
+ if (readMember(value, "serverOnly") === true)
981
+ plan.serverOnly = true;
982
+ return plan;
983
+ }
984
+ function parseLiveExpression(value) {
985
+ const operator = stringMember(value, "operator");
986
+ if (!operator || ![
987
+ "eq", "neq", "gt", "gte", "lt", "lte", "in", "contains",
988
+ "containsInsensitive", "range", "and", "or", "not", "server",
989
+ ].includes(operator))
990
+ return undefined;
991
+ const expression = { operator: operator };
992
+ const column = stringMember(value, "column");
993
+ if (column)
994
+ expression.column = column;
995
+ const parsedValue = parseLiveValue(readMember(value, "value"));
996
+ if (parsedValue)
997
+ expression.value = parsedValue;
998
+ const valueTo = parseLiveValue(readMember(value, "valueTo"));
999
+ if (valueTo)
1000
+ expression.valueTo = valueTo;
1001
+ const childrenValue = readMember(value, "children");
1002
+ if (Array.isArray(childrenValue)) {
1003
+ const children = childrenValue.map(parseLiveExpression).filter((child) => child !== undefined);
1004
+ if (children.length > 0)
1005
+ expression.children = children;
1006
+ }
1007
+ return expression;
1008
+ }
1009
+ function parseLiveValue(value) {
1010
+ const argument = stringMember(value, "argument");
1011
+ if (argument)
1012
+ return { argument };
1013
+ const literal = readMember(value, "literal");
1014
+ return literal === undefined ? undefined : { literal };
1015
+ }
1016
+ function replicaFromOptions(options) {
1017
+ const value = options?.get("replica")?.value;
1018
+ const table = stringMember(value, "table");
1019
+ if (!table)
1020
+ return undefined;
1021
+ const definition = {
1022
+ table,
1023
+ key: stringMember(value, "key") ?? "id",
1024
+ columns: stringArray(readMember(value, "columns")) ?? [],
1025
+ };
1026
+ const equalFilters = readMember(value, "equalFilters");
1027
+ if (equalFilters && typeof equalFilters === "object" && !Array.isArray(equalFilters)) {
1028
+ const filters = {};
1029
+ for (const [argument, column] of Object.entries(equalFilters)) {
1030
+ if (typeof column === "string")
1031
+ filters[argument] = column;
1032
+ }
1033
+ if (Object.keys(filters).length > 0)
1034
+ definition.equalFilters = filters;
1035
+ }
1036
+ const excludeWhenSet = stringArray(readMember(value, "excludeWhenSet"));
1037
+ if (excludeWhenSet)
1038
+ definition.excludeWhenSet = excludeWhenSet;
1039
+ const visibilityTables = stringArray(readMember(value, "visibilityTables"));
1040
+ if (visibilityTables)
1041
+ definition.visibilityTables = visibilityTables;
1042
+ const orderBy = stringMember(value, "orderBy");
1043
+ if (orderBy) {
1044
+ definition.orderBy = orderBy;
1045
+ definition.orderDirection = stringMember(value, "orderDirection")?.toLowerCase() === "asc" ? "asc" : "desc";
1046
+ }
1047
+ definition.mode = stringMember(value, "mode") === "progressive" ? "progressive" : "eager";
1048
+ const maxRows = numberMember(value, "maxRows");
1049
+ if (maxRows !== undefined && maxRows > 0)
1050
+ definition.maxRows = maxRows;
1051
+ const maxBytes = numberMember(value, "maxBytes");
1052
+ if (maxBytes !== undefined && maxBytes > 0)
1053
+ definition.maxBytes = maxBytes;
1054
+ if (!definition.columns.includes(definition.key))
1055
+ definition.columns.push(definition.key);
1056
+ return definition;
1057
+ }
1058
+ function readCallArguments(source, openParen) {
1059
+ const close = findMatching(source, openParen);
1060
+ if (close < 0)
1061
+ return { args: [], end: openParen + 1 };
1062
+ const args = [];
1063
+ let cursor = openParen + 1;
1064
+ while (cursor < close) {
1065
+ cursor = skipTrivia(source, cursor);
1066
+ if (cursor >= close)
1067
+ break;
1068
+ const argument = readValue(source, cursor);
1069
+ if (argument.end <= cursor)
1070
+ break;
1071
+ args.push(argument);
1072
+ cursor = skipTrivia(source, argument.end);
1073
+ if (source[cursor] === ",")
1074
+ cursor += 1;
1075
+ }
1076
+ return { args, end: close + 1 };
1077
+ }
1078
+ function readValue(source, start) {
1079
+ const begin = skipTrivia(source, start);
1080
+ const char = source[begin];
1081
+ if (char === undefined)
1082
+ return { text: "", end: source.length };
1083
+ if (char === "{") {
1084
+ const close = findMatching(source, begin);
1085
+ if (close < 0)
1086
+ return { text: source.slice(begin), end: source.length };
1087
+ const entries = parseObjectEntries(source, begin, close);
1088
+ const object = {};
1089
+ let literal = true;
1090
+ for (const [key, entry] of entries) {
1091
+ if (entry.value === undefined) {
1092
+ literal = false;
1093
+ break;
1094
+ }
1095
+ object[key] = entry.value;
1096
+ }
1097
+ return { ...(literal ? { value: object } : {}), entries, text: source.slice(begin, close + 1), end: close + 1 };
1098
+ }
1099
+ if (char === "[") {
1100
+ const close = findMatching(source, begin);
1101
+ if (close < 0)
1102
+ return { text: source.slice(begin), end: source.length };
1103
+ const items = [];
1104
+ let literal = true;
1105
+ let cursor = begin + 1;
1106
+ while (cursor < close) {
1107
+ cursor = skipTrivia(source, cursor);
1108
+ if (cursor >= close)
1109
+ break;
1110
+ const item = readValue(source, cursor);
1111
+ if (item.end <= cursor)
1112
+ break;
1113
+ if (item.value === undefined)
1114
+ literal = false;
1115
+ else
1116
+ items.push(item.value);
1117
+ cursor = skipTrivia(source, item.end);
1118
+ if (source[cursor] === ",")
1119
+ cursor += 1;
1120
+ }
1121
+ return { ...(literal ? { value: items } : {}), text: source.slice(begin, close + 1), end: close + 1 };
1122
+ }
1123
+ if (char === '"' || char === "'" || char === "`") {
1124
+ const string = readStringLiteral(source, begin);
1125
+ const end = string?.end ?? source.length;
1126
+ return { ...(string?.value === undefined ? {} : { value: string.value }), text: source.slice(begin, end), end };
1127
+ }
1128
+ keywordPattern.lastIndex = begin;
1129
+ const keyword = keywordPattern.exec(source);
1130
+ if (keyword) {
1131
+ const end = begin + keyword[0].length;
1132
+ const value = keyword[0] === "true" ? true : keyword[0] === "false" ? false : keyword[0] === "null" ? null : undefined;
1133
+ return { ...(keyword[0] === "undefined" ? {} : { value }), text: keyword[0], end };
1134
+ }
1135
+ numberPattern.lastIndex = begin;
1136
+ const number = numberPattern.exec(source);
1137
+ if (number) {
1138
+ const parsed = Number(number[0].replace(/_/g, ""));
1139
+ const end = begin + number[0].length;
1140
+ return { ...(Number.isFinite(parsed) ? { value: parsed } : {}), text: number[0], end };
1141
+ }
1142
+ // Identifiers, calls, and arrow functions are kept as source text: the CLI
1143
+ // records what the module declared without pretending to evaluate it.
1144
+ let cursor = begin;
1145
+ while (cursor < source.length) {
1146
+ const current = source[cursor];
1147
+ const next = source[cursor + 1] ?? "";
1148
+ if (current === "/" && next === "/") {
1149
+ const newline = source.indexOf("\n", cursor + 2);
1150
+ cursor = newline < 0 ? source.length : newline + 1;
1151
+ continue;
1152
+ }
1153
+ if (current === "/" && next === "*") {
1154
+ const closeComment = source.indexOf("*/", cursor + 2);
1155
+ cursor = closeComment < 0 ? source.length : closeComment + 2;
1156
+ continue;
1157
+ }
1158
+ if (current === '"' || current === "'" || current === "`") {
1159
+ const string = readStringLiteral(source, cursor);
1160
+ cursor = string ? string.end : cursor + 1;
1161
+ continue;
1162
+ }
1163
+ if (current === "{" || current === "[" || current === "(") {
1164
+ const closeBracket = findMatching(source, cursor);
1165
+ cursor = closeBracket < 0 ? source.length : closeBracket + 1;
1166
+ continue;
1167
+ }
1168
+ if (current === "," || current === "}" || current === "]" || current === ")")
1169
+ break;
1170
+ cursor += 1;
1171
+ }
1172
+ return { text: source.slice(begin, cursor).trim(), end: cursor };
1173
+ }
1174
+ function parseObjectEntries(source, open, close) {
1175
+ const entries = new Map();
1176
+ let cursor = open + 1;
1177
+ while (cursor < close) {
1178
+ cursor = skipTrivia(source, cursor);
1179
+ if (cursor >= close)
1180
+ break;
1181
+ const char = source[cursor];
1182
+ if (char === "," || char === ";") {
1183
+ cursor += 1;
1184
+ continue;
1185
+ }
1186
+ if (char === ".") {
1187
+ // Spread members contribute nothing the CLI can resolve statically.
1188
+ const spread = readValue(source, cursor);
1189
+ cursor = spread.end > cursor ? spread.end : cursor + 1;
1190
+ continue;
1191
+ }
1192
+ const key = readMemberKey(source, cursor);
1193
+ if (!key)
1194
+ break;
1195
+ const afterKey = skipTrivia(source, key.end);
1196
+ if (source[afterKey] === ":") {
1197
+ const value = readValue(source, afterKey + 1);
1198
+ entries.set(key.key, { ...(value.value === undefined ? {} : { value: value.value }), text: value.text });
1199
+ cursor = value.end > afterKey ? value.end : afterKey + 1;
1200
+ continue;
1201
+ }
1202
+ if (source[afterKey] === "(") {
1203
+ // Method shorthand: keep the signature only, so the declared parameter
1204
+ // and return types stay readable without the body.
1205
+ const closeParen = findMatching(source, afterKey);
1206
+ if (closeParen < 0)
1207
+ break;
1208
+ const body = indexOfTopLevel(source, "{", closeParen + 1, close);
1209
+ const signatureEnd = body < 0 ? closeParen + 1 : body;
1210
+ entries.set(key.key, { text: source.slice(key.end, signatureEnd).trim() });
1211
+ const bodyEnd = body < 0 ? -1 : findMatching(source, body);
1212
+ cursor = bodyEnd < 0 ? signatureEnd : bodyEnd + 1;
1213
+ continue;
1214
+ }
1215
+ entries.set(key.key, { text: key.key });
1216
+ cursor = afterKey;
1217
+ }
1218
+ return entries;
1219
+ }
1220
+ function readMemberKey(source, start) {
1221
+ let cursor = skipTrivia(source, start);
1222
+ // `async`, `get`, and `set` prefix method shorthands; the key follows them.
1223
+ for (let guard = 0; guard < 3; guard += 1) {
1224
+ const char = source[cursor];
1225
+ if (char === undefined)
1226
+ return undefined;
1227
+ if (char === '"' || char === "'" || char === "`") {
1228
+ const string = readStringLiteral(source, cursor);
1229
+ if (!string || string.value === undefined)
1230
+ return undefined;
1231
+ return { key: string.value, end: string.end };
1232
+ }
1233
+ identifierPattern.lastIndex = cursor;
1234
+ const identifier = identifierPattern.exec(source);
1235
+ if (!identifier)
1236
+ return undefined;
1237
+ const name = identifier[0];
1238
+ const after = skipTrivia(source, cursor + name.length);
1239
+ const modifier = (name === "async" || name === "get" || name === "set") && /[A-Za-z_$"'`]/.test(source[after] ?? "");
1240
+ if (!modifier)
1241
+ return { key: name, end: cursor + name.length };
1242
+ cursor = after;
1243
+ }
1244
+ return undefined;
1245
+ }
1246
+ function readStringLiteral(source, start) {
1247
+ const quote = source[start];
1248
+ if (quote !== '"' && quote !== "'" && quote !== "`")
1249
+ return undefined;
1250
+ let interpolated = false;
1251
+ let raw = "";
1252
+ let cursor = start + 1;
1253
+ while (cursor < source.length) {
1254
+ const char = source[cursor];
1255
+ if (char === "\\") {
1256
+ raw += source.slice(cursor, cursor + 2);
1257
+ cursor += 2;
1258
+ continue;
1259
+ }
1260
+ if (quote === "`" && char === "$" && source[cursor + 1] === "{") {
1261
+ const close = findMatching(source, cursor + 1);
1262
+ if (close < 0)
1263
+ return { end: source.length };
1264
+ interpolated = true;
1265
+ cursor = close + 1;
1266
+ continue;
1267
+ }
1268
+ if (char === quote) {
1269
+ return interpolated ? { end: cursor + 1 } : { value: decodeStringLiteral(raw), end: cursor + 1 };
1270
+ }
1271
+ raw += char;
1272
+ cursor += 1;
1273
+ }
1274
+ return { end: source.length };
1275
+ }
1276
+ function decodeStringLiteral(raw) {
1277
+ let decoded = "";
1278
+ for (let index = 0; index < raw.length; index += 1) {
1279
+ const char = raw[index];
1280
+ if (char !== "\\") {
1281
+ decoded += char;
1282
+ continue;
1283
+ }
1284
+ const escape = raw[index + 1] ?? "";
1285
+ index += 1;
1286
+ if (escape === "n")
1287
+ decoded += "\n";
1288
+ else if (escape === "r")
1289
+ decoded += "\r";
1290
+ else if (escape === "t")
1291
+ decoded += "\t";
1292
+ else if (escape === "b")
1293
+ decoded += "\b";
1294
+ else if (escape === "f")
1295
+ decoded += "\f";
1296
+ else if (escape === "v")
1297
+ decoded += "\v";
1298
+ else if (escape === "0")
1299
+ decoded += "\0";
1300
+ else if (escape === "x") {
1301
+ const code = Number.parseInt(raw.slice(index + 1, index + 3), 16);
1302
+ if (Number.isFinite(code)) {
1303
+ decoded += String.fromCharCode(code);
1304
+ index += 2;
1305
+ }
1306
+ }
1307
+ else if (escape === "u") {
1308
+ if (raw[index + 1] === "{") {
1309
+ const close = raw.indexOf("}", index + 2);
1310
+ const code = close < 0 ? Number.NaN : Number.parseInt(raw.slice(index + 2, close), 16);
1311
+ if (Number.isFinite(code) && code <= 0x10ffff) {
1312
+ decoded += String.fromCodePoint(code);
1313
+ index = close;
1314
+ }
1315
+ }
1316
+ else {
1317
+ const code = Number.parseInt(raw.slice(index + 1, index + 5), 16);
1318
+ if (Number.isFinite(code)) {
1319
+ decoded += String.fromCharCode(code);
1320
+ index += 4;
1321
+ }
1322
+ }
1323
+ }
1324
+ else {
1325
+ decoded += escape;
1326
+ }
1327
+ }
1328
+ return decoded;
1329
+ }
1330
+ /**
1331
+ * Balanced scanner over braces, brackets, and parentheses that skips strings
1332
+ * and comments. Regular-expression literals are not tracked; declarative module
1333
+ * metadata does not use them.
1334
+ */
1335
+ function findMatching(source, open) {
1336
+ const opener = source[open];
1337
+ if (opener !== "{" && opener !== "[" && opener !== "(")
1338
+ return -1;
1339
+ let depth = 0;
1340
+ let cursor = open;
1341
+ while (cursor < source.length) {
1342
+ const char = source[cursor];
1343
+ const next = source[cursor + 1] ?? "";
1344
+ if (char === "/" && next === "/") {
1345
+ const newline = source.indexOf("\n", cursor + 2);
1346
+ cursor = newline < 0 ? source.length : newline + 1;
1347
+ continue;
1348
+ }
1349
+ if (char === "/" && next === "*") {
1350
+ const closeComment = source.indexOf("*/", cursor + 2);
1351
+ cursor = closeComment < 0 ? source.length : closeComment + 2;
1352
+ continue;
1353
+ }
1354
+ if (char === '"' || char === "'" || char === "`") {
1355
+ const string = readStringLiteral(source, cursor);
1356
+ cursor = string ? string.end : cursor + 1;
1357
+ continue;
1358
+ }
1359
+ if (char === "{" || char === "[" || char === "(") {
1360
+ depth += 1;
1361
+ cursor += 1;
1362
+ continue;
1363
+ }
1364
+ if (char === "}" || char === "]" || char === ")") {
1365
+ depth -= 1;
1366
+ if (depth === 0)
1367
+ return cursor;
1368
+ cursor += 1;
1369
+ continue;
1370
+ }
1371
+ cursor += 1;
1372
+ }
1373
+ return -1;
1374
+ }
1375
+ function indexOfTopLevel(source, token, from, limit = source.length) {
1376
+ let cursor = from;
1377
+ while (cursor < limit) {
1378
+ if (source.startsWith(token, cursor))
1379
+ return cursor;
1380
+ const char = source[cursor];
1381
+ const next = source[cursor + 1] ?? "";
1382
+ if (char === "/" && next === "/") {
1383
+ const newline = source.indexOf("\n", cursor + 2);
1384
+ cursor = newline < 0 ? limit : newline + 1;
1385
+ continue;
1386
+ }
1387
+ if (char === "/" && next === "*") {
1388
+ const closeComment = source.indexOf("*/", cursor + 2);
1389
+ cursor = closeComment < 0 ? limit : closeComment + 2;
1390
+ continue;
1391
+ }
1392
+ if (char === '"' || char === "'" || char === "`") {
1393
+ const string = readStringLiteral(source, cursor);
1394
+ cursor = string ? string.end : cursor + 1;
1395
+ continue;
1396
+ }
1397
+ if (char === "{" || char === "[" || char === "(") {
1398
+ const closeBracket = findMatching(source, cursor);
1399
+ cursor = closeBracket < 0 ? limit : closeBracket + 1;
1400
+ continue;
1401
+ }
1402
+ if (char === "}" || char === "]" || char === ")")
1403
+ return -1;
1404
+ cursor += 1;
1405
+ }
1406
+ return -1;
1407
+ }
1408
+ function splitTopLevel(text) {
1409
+ const parts = [];
1410
+ let start = 0;
1411
+ let cursor = 0;
1412
+ while (cursor < text.length) {
1413
+ const char = text[cursor];
1414
+ const next = text[cursor + 1] ?? "";
1415
+ if (char === "/" && next === "/") {
1416
+ const newline = text.indexOf("\n", cursor + 2);
1417
+ cursor = newline < 0 ? text.length : newline + 1;
1418
+ continue;
1419
+ }
1420
+ if (char === "/" && next === "*") {
1421
+ const closeComment = text.indexOf("*/", cursor + 2);
1422
+ cursor = closeComment < 0 ? text.length : closeComment + 2;
1423
+ continue;
1424
+ }
1425
+ if (char === '"' || char === "'" || char === "`") {
1426
+ const string = readStringLiteral(text, cursor);
1427
+ cursor = string ? string.end : cursor + 1;
1428
+ continue;
1429
+ }
1430
+ if (char === "{" || char === "[" || char === "(") {
1431
+ const closeBracket = findMatching(text, cursor);
1432
+ cursor = closeBracket < 0 ? text.length : closeBracket + 1;
1433
+ continue;
1434
+ }
1435
+ if (char === "<") {
1436
+ const closeAngle = findMatchingAngle(text, cursor);
1437
+ cursor = closeAngle < 0 ? cursor + 1 : closeAngle + 1;
1438
+ continue;
1439
+ }
1440
+ if (char === ",") {
1441
+ parts.push(text.slice(start, cursor).trim());
1442
+ start = cursor + 1;
1443
+ }
1444
+ cursor += 1;
1445
+ }
1446
+ parts.push(text.slice(start).trim());
1447
+ return parts.filter((part) => part.length > 0);
1448
+ }
1449
+ function findMatchingAngle(text, open) {
1450
+ let depth = 0;
1451
+ let cursor = open;
1452
+ while (cursor < text.length) {
1453
+ const char = text[cursor];
1454
+ if (char === "=" && text[cursor + 1] === ">") {
1455
+ cursor += 2;
1456
+ continue;
1457
+ }
1458
+ if (char === "<") {
1459
+ depth += 1;
1460
+ cursor += 1;
1461
+ continue;
1462
+ }
1463
+ if (char === ">") {
1464
+ depth -= 1;
1465
+ if (depth === 0)
1466
+ return cursor;
1467
+ cursor += 1;
1468
+ continue;
1469
+ }
1470
+ if (char === "{" || char === "[" || char === "(") {
1471
+ const closeBracket = findMatching(text, cursor);
1472
+ if (closeBracket < 0)
1473
+ return -1;
1474
+ cursor = closeBracket + 1;
1475
+ continue;
1476
+ }
1477
+ cursor += 1;
1478
+ }
1479
+ return -1;
1480
+ }
1481
+ function skipTrivia(source, start) {
1482
+ let cursor = start;
1483
+ while (cursor < source.length) {
1484
+ const char = source[cursor];
1485
+ const next = source[cursor + 1] ?? "";
1486
+ if (/\s/.test(char)) {
1487
+ cursor += 1;
1488
+ continue;
1489
+ }
1490
+ if (char === "/" && next === "/") {
1491
+ const newline = source.indexOf("\n", cursor + 2);
1492
+ if (newline < 0)
1493
+ return source.length;
1494
+ cursor = newline + 1;
1495
+ continue;
1496
+ }
1497
+ if (char === "/" && next === "*") {
1498
+ const closeComment = source.indexOf("*/", cursor + 2);
1499
+ if (closeComment < 0)
1500
+ return source.length;
1501
+ cursor = closeComment + 2;
1502
+ continue;
1503
+ }
1504
+ break;
1505
+ }
1506
+ return cursor;
1507
+ }
1508
+ function identifierText(text) {
1509
+ const trimmed = text?.trim();
1510
+ return trimmed && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(trimmed) ? trimmed : undefined;
1511
+ }
1512
+ function stringEntry(options, key) {
1513
+ const value = options?.get(key)?.value;
1514
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
1515
+ }
1516
+ function readMember(value, key) {
1517
+ if (!value || typeof value !== "object" || Array.isArray(value))
1518
+ return undefined;
1519
+ return value[key];
1520
+ }
1521
+ function stringMember(value, key) {
1522
+ const member = readMember(value, key);
1523
+ return typeof member === "string" && member.trim() ? member.trim() : undefined;
1524
+ }
1525
+ function numberMember(value, key) {
1526
+ const member = readMember(value, key);
1527
+ return typeof member === "number" && Number.isFinite(member) ? member : undefined;
1528
+ }
1529
+ function stringArray(value) {
1530
+ if (typeof value === "string")
1531
+ return value.trim() ? [value.trim()] : undefined;
1532
+ if (!Array.isArray(value))
1533
+ return undefined;
1534
+ const values = value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
1535
+ return values.length > 0 ? values : undefined;
1536
+ }
1537
+ function pathArray(value) {
1538
+ if (typeof value === "string")
1539
+ return value.split(".").map((segment) => segment.trim()).filter(Boolean);
1540
+ return stringArray(value) ?? [];
1541
+ }
1542
+ function functionPathPrefix(backendDir, file) {
1543
+ const withoutExtension = relative(backendDir, file).replace(/\\/g, "/").replace(/\.(?:tsx|ts|mts|cts)$/, "");
1544
+ const segments = withoutExtension.split("/").filter((segment) => segment && segment !== ".");
1545
+ if (segments[segments.length - 1] === "index")
1546
+ segments.pop();
1547
+ return segments.join(".");
1548
+ }
1549
+ function projectPath(root, file) {
1550
+ return relative(root, file).replace(/\\/g, "/");
1551
+ }
1552
+ function sortedRecord(record) {
1553
+ const sorted = {};
1554
+ for (const key of Object.keys(record).sort())
1555
+ sorted[key] = record[key];
1556
+ return sorted;
1557
+ }
1558
+ function isModuleSourceFile(name) {
1559
+ if (skippedSourceFiles.has(name))
1560
+ return false;
1561
+ if (/\.d\.(?:ts|mts|cts)$/.test(name))
1562
+ return false;
1563
+ if (/\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(name))
1564
+ return false;
1565
+ return moduleSourceExtensions.some((extension) => name.endsWith(extension));
1566
+ }
1567
+ async function walkFiles(dir, accept) {
1568
+ if (!existsSync(dir))
1569
+ return [];
1570
+ const entries = await readdir(dir, { withFileTypes: true });
1571
+ const files = [];
1572
+ for (const entry of entries) {
1573
+ const path = join(dir, entry.name);
1574
+ if (entry.isDirectory()) {
1575
+ if (skippedDirectories.has(entry.name) || entry.name.startsWith("."))
1576
+ continue;
1577
+ files.push(...await walkFiles(path, accept));
1578
+ }
1579
+ else if (entry.isFile() && accept(entry.name)) {
1580
+ files.push(path);
1581
+ }
1582
+ }
1583
+ return files.sort();
1584
+ }
1585
+ //# sourceMappingURL=module-artifact.js.map