zodvex 0.7.2-beta.0 → 0.7.2

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.
package/dist/cli/index.js CHANGED
@@ -3,7 +3,8 @@ import fs2, { readFileSync, writeFileSync, existsSync } from 'fs';
3
3
  import path3, { resolve, relative, join } from 'path';
4
4
  import { Project, SyntaxKind } from 'ts-morph';
5
5
  import { globSync } from 'tinyglobby';
6
- import { pathToFileURL } from 'url';
6
+ import { spawn } from 'child_process';
7
+ import { fileURLToPath } from 'url';
7
8
  import { z } from 'zod';
8
9
  import { $ZodCodec, $ZodNumber, $ZodCustom, $ZodType, $ZodOptional, $ZodNullable, $ZodObject, $ZodUnion, $ZodArray, $ZodRecord, $ZodTuple, clone, $ZodString, $ZodBoolean, $ZodNull, $ZodUndefined, $ZodAny, $ZodEnum, $ZodLiteral, $ZodDiscriminatedUnion } from 'zod/v4/core';
9
10
  import 'convex/values';
@@ -1028,6 +1029,20 @@ function readMeta(target) {
1028
1029
  }
1029
1030
  return target[META_KEY];
1030
1031
  }
1032
+ var CODEC_BRAND_KEY = "__zodvexCodecBrand";
1033
+ function attachCodecBrand(target, brand) {
1034
+ Object.defineProperty(target, CODEC_BRAND_KEY, {
1035
+ value: brand,
1036
+ enumerable: false,
1037
+ writable: false,
1038
+ configurable: false
1039
+ });
1040
+ }
1041
+ function readCodecBrand(target) {
1042
+ if (target == null || typeof target !== "object") return void 0;
1043
+ const value = target[CODEC_BRAND_KEY];
1044
+ return typeof value === "string" ? value : void 0;
1045
+ }
1031
1046
  var metadata = /* @__PURE__ */ new WeakMap();
1032
1047
  var registryHelpers = {
1033
1048
  getMetadata: (type) => metadata.get(type),
@@ -1068,8 +1083,10 @@ function id(tableName) {
1068
1083
  branded._tableName = tableName;
1069
1084
  return branded;
1070
1085
  }
1071
- function codec(wire, runtime, transforms) {
1072
- return zodvexCodec(wire, runtime, transforms);
1086
+ function codec(wire, runtime, transforms, opts) {
1087
+ const built = zodvexCodec(wire, runtime, transforms);
1088
+ if (opts?.brand) attachCodecBrand(built, opts.brand);
1089
+ return built;
1073
1090
  }
1074
1091
  function sharedWeakMap(name) {
1075
1092
  const key = /* @__PURE__ */ Symbol.for(`zodvex.zx.cache.${name}`);
@@ -1394,7 +1411,6 @@ function findCodec(schema) {
1394
1411
  }
1395
1412
 
1396
1413
  // src/public/codegen/discover.ts
1397
- var discoveryRunCounter = 0;
1398
1414
  function walkSchemaRecursive(schema, accessPath, visited, seenCodecs, results) {
1399
1415
  if (visited.has(schema)) return;
1400
1416
  visited.add(schema);
@@ -1532,7 +1548,7 @@ function walkFunctionCodecs(functions) {
1532
1548
  }
1533
1549
  return found;
1534
1550
  }
1535
- async function discoverModules(convexDir2, options) {
1551
+ async function discoverModules(convexDir2) {
1536
1552
  const models = [];
1537
1553
  const functions = [];
1538
1554
  const codecs = [];
@@ -1556,18 +1572,12 @@ async function discoverModules(convexDir2, options) {
1556
1572
  "crons.js"
1557
1573
  ]
1558
1574
  }).sort();
1559
- let cacheKey = null;
1560
- if (options?.freshImports) {
1561
- discoveryRunCounter += 1;
1562
- cacheKey = `${process.pid}-${discoveryRunCounter}`;
1563
- }
1564
1575
  try {
1565
1576
  for (const file of files) {
1566
1577
  const absPath = path3.resolve(convexDir2, file);
1567
- const importUrl = cacheKey ? `${pathToFileURL(absPath).href}?t=${cacheKey}` : absPath;
1568
1578
  let moduleExports;
1569
1579
  try {
1570
- moduleExports = await import(importUrl);
1580
+ moduleExports = await import(absPath);
1571
1581
  } catch (err) {
1572
1582
  console.warn(`[zodvex] Warning: Failed to import ${file}:`, err.message);
1573
1583
  continue;
@@ -1722,7 +1732,10 @@ var HEADER = `// AUTO-GENERATED by zodvex \u2014 do not edit
1722
1732
  `;
1723
1733
  function fingerprintCodec(schema) {
1724
1734
  if (!(schema instanceof $ZodCodec)) return "";
1725
- return `${fingerprintLeaf(schema._zod.def.in)}|${fingerprintLeaf(schema._zod.def.out)}`;
1735
+ const def = schema._zod.def;
1736
+ const transform = typeof def.transform === "function" ? def.transform.toString() : "";
1737
+ const reverse = typeof def.reverseTransform === "function" ? def.reverseTransform.toString() : "";
1738
+ return `${fingerprintLeaf(def.in)}|${fingerprintLeaf(def.out)}|${transform}|${reverse}`;
1726
1739
  }
1727
1740
  function fingerprintLeaf(schema) {
1728
1741
  return `${zodToSource(schema)}#${fingerprintChecks(schema)}`;
@@ -1941,6 +1954,7 @@ function generateApiFile(functions, models, codecs, modelCodecs, functionCodecs,
1941
1954
  }
1942
1955
  if (functionCodecs) {
1943
1956
  const fingerprintMap = /* @__PURE__ */ new Map();
1957
+ const brandMap = /* @__PURE__ */ new Map();
1944
1958
  const codecSchemaToSourceFile = /* @__PURE__ */ new Map();
1945
1959
  for (const c of codecs ?? []) {
1946
1960
  codecSchemaToSourceFile.set(c.schema, c.sourceFile);
@@ -1949,24 +1963,34 @@ function generateApiFile(functions, models, codecs, modelCodecs, functionCodecs,
1949
1963
  codecSchemaToSourceFile.set(mc.codec, mc.modelSourceFile);
1950
1964
  }
1951
1965
  for (const [codecSchema, ref] of codecMap) {
1952
- const fp = fingerprintCodec(codecSchema);
1953
- if (!fp) continue;
1954
1966
  const sourceFile = codecSchemaToSourceFile.get(codecSchema);
1955
- const existing = fingerprintMap.get(fp);
1956
- if (existing) {
1957
- existing.push({ ref, sourceFile });
1958
- } else {
1959
- fingerprintMap.set(fp, [{ ref, sourceFile }]);
1967
+ const brand = readCodecBrand(codecSchema);
1968
+ const candidate = { ref, sourceFile, brand };
1969
+ const fp = fingerprintCodec(codecSchema);
1970
+ if (fp) {
1971
+ const existing = fingerprintMap.get(fp);
1972
+ if (existing) existing.push(candidate);
1973
+ else fingerprintMap.set(fp, [candidate]);
1974
+ }
1975
+ if (brand) {
1976
+ const existing = brandMap.get(brand);
1977
+ if (existing) existing.push(candidate);
1978
+ else brandMap.set(brand, [candidate]);
1960
1979
  }
1961
1980
  }
1981
+ const undiscoverable = [];
1962
1982
  for (const fc of functionCodecs) {
1963
1983
  if (codecMap.has(fc.codec)) continue;
1964
- const fp = fingerprintCodec(fc.codec);
1965
- const candidates = fp ? fingerprintMap.get(fp) : void 0;
1984
+ const brand = readCodecBrand(fc.codec);
1985
+ let candidates = brand ? brandMap.get(brand) : void 0;
1966
1986
  if (!candidates || candidates.length === 0) {
1967
- console.warn(
1968
- `[zodvex] Warning: Codec in ${fc.functionExportName}() (${fc.accessPath}) has no matching model or exported codec. Export it standalone for full client-side codec support.`
1987
+ const fp = fingerprintCodec(fc.codec);
1988
+ candidates = (fp ? fingerprintMap.get(fp) : void 0)?.filter(
1989
+ (c) => c.brand === void 0 || c.brand === brand
1969
1990
  );
1991
+ }
1992
+ if (!candidates || candidates.length === 0) {
1993
+ undiscoverable.push({ fn: fc.functionExportName, path: fc.accessPath });
1970
1994
  continue;
1971
1995
  }
1972
1996
  let chosen;
@@ -1977,14 +2001,27 @@ function generateApiFile(functions, models, codecs, modelCodecs, functionCodecs,
1977
2001
  if (sameFile.length === 1) {
1978
2002
  chosen = sameFile[0].ref;
1979
2003
  } else {
1980
- console.warn(
1981
- `[zodvex] Warning: Codec in ${fc.functionExportName}() (${fc.accessPath}) matches ${candidates.length} candidates with the same fingerprint (${candidates.map((c) => c.ref.exportName).join(", ")}). Cannot pick a canonical reference \u2014 emitting inline. Export the codec standalone to disambiguate.`
2004
+ const sorted = [...candidates].sort(
2005
+ (a, b) => `${a.sourceFile ?? ""}|${a.ref.exportName}`.localeCompare(
2006
+ `${b.sourceFile ?? ""}|${b.ref.exportName}`
2007
+ )
1982
2008
  );
2009
+ chosen = sorted[0].ref;
2010
+ if (!brand) {
2011
+ console.warn(
2012
+ `[zodvex] Note: Codec in ${fc.functionExportName}() (${fc.accessPath}) matches ${candidates.length} fingerprint-equivalent codecs (${candidates.map((c) => c.ref.exportName).join(", ")}). Referencing '${chosen.exportName}'. Export the codec standalone or give it a brand if you want the reference to be explicit.`
2013
+ );
2014
+ }
1983
2015
  }
1984
2016
  }
1985
- if (chosen) {
1986
- codecMap.set(fc.codec, chosen);
1987
- }
2017
+ codecMap.set(fc.codec, chosen);
2018
+ }
2019
+ if (undiscoverable.length > 0) {
2020
+ const list = undiscoverable.map((u) => ` - ${u.fn}() at ${u.path}`).join("\n");
2021
+ throw new Error(
2022
+ `[zodvex] ${undiscoverable.length} codec(s) in function args/returns have no importable reference (not exported standalone, not embedded in a model). The generated client cannot encode or decode them, which silently breaks the codec boundary. Export each codec standalone (or add it to a model) so codegen can import it:
2023
+ ${list}`
2024
+ );
1988
2025
  }
1989
2026
  }
1990
2027
  const zodToSourceCtx = {
@@ -2156,7 +2193,7 @@ async function generate(convexDir2, options) {
2156
2193
  const resolved = resolveConvexDir(convexDir2);
2157
2194
  const zodvexDir = path3.join(resolved, "_zodvex");
2158
2195
  writeStubApi(zodvexDir);
2159
- const result = await discoverModules(resolved, { freshImports: options?.freshImports });
2196
+ const result = await discoverModules(resolved);
2160
2197
  const schemaContent = generateSchemaFile(result.models);
2161
2198
  const apiContent = generateApiFile(
2162
2199
  result.functions,
@@ -2193,13 +2230,9 @@ async function dev(convexDir2, options) {
2193
2230
  return;
2194
2231
  }
2195
2232
  if (debounceTimer) clearTimeout(debounceTimer);
2196
- debounceTimer = setTimeout(async () => {
2233
+ debounceTimer = setTimeout(() => {
2197
2234
  console.log("[zodvex] Regenerating...");
2198
- try {
2199
- await generate(resolved, { ...options, freshImports: true });
2200
- } catch (err) {
2201
- console.error("[zodvex] Generation failed:", err.message);
2202
- }
2235
+ void regenerate(resolved, options);
2203
2236
  }, 300);
2204
2237
  });
2205
2238
  process.on("SIGINT", () => {
@@ -2208,6 +2241,22 @@ async function dev(convexDir2, options) {
2208
2241
  process.exit(0);
2209
2242
  });
2210
2243
  }
2244
+ function regenerate(resolved, options) {
2245
+ const cliEntry = fileURLToPath(new URL("./index.js", import.meta.url));
2246
+ const args = [cliEntry, "generate", resolved];
2247
+ if (options?.mini) args.push("--mini");
2248
+ return new Promise((resolve2) => {
2249
+ const child = spawn(process.execPath, args, { stdio: "inherit" });
2250
+ child.on("exit", (code) => {
2251
+ if (code !== 0) console.error(`[zodvex] Regeneration exited with code ${code}`);
2252
+ resolve2();
2253
+ });
2254
+ child.on("error", (err) => {
2255
+ console.error("[zodvex] Failed to spawn regeneration:", err.message);
2256
+ resolve2();
2257
+ });
2258
+ });
2259
+ }
2211
2260
  function writeStubApi(zodvexDir) {
2212
2261
  fs2.mkdirSync(zodvexDir, { recursive: true });
2213
2262
  fs2.writeFileSync(