zodvex 0.7.1 → 0.7.2-beta.1

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 (48) hide show
  1. package/dist/cli/index.js +186 -24
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/codegen/index.js +137 -14
  4. package/dist/codegen/index.js.map +1 -1
  5. package/dist/core/index.js +56 -10
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/index.js +56 -10
  8. package/dist/index.js.map +1 -1
  9. package/dist/internal/db.d.ts +14 -11
  10. package/dist/internal/db.d.ts.map +1 -1
  11. package/dist/internal/meta.d.ts +11 -0
  12. package/dist/internal/meta.d.ts.map +1 -1
  13. package/dist/internal/model.d.ts +21 -0
  14. package/dist/internal/model.d.ts.map +1 -1
  15. package/dist/internal/zx.d.ts +14 -0
  16. package/dist/internal/zx.d.ts.map +1 -1
  17. package/dist/labs/index.js +29 -4
  18. package/dist/labs/index.js.map +1 -1
  19. package/dist/legacy/index.js +13 -2
  20. package/dist/legacy/index.js.map +1 -1
  21. package/dist/mini/index.js +58 -12
  22. package/dist/mini/index.js.map +1 -1
  23. package/dist/mini/server/index.js +37 -27
  24. package/dist/mini/server/index.js.map +1 -1
  25. package/dist/public/cli/commands.d.ts +12 -0
  26. package/dist/public/cli/commands.d.ts.map +1 -1
  27. package/dist/public/codegen/discover.d.ts +8 -0
  28. package/dist/public/codegen/discover.d.ts.map +1 -1
  29. package/dist/public/codegen/generate.d.ts.map +1 -1
  30. package/dist/public/mini/model.d.ts +6 -0
  31. package/dist/public/mini/model.d.ts.map +1 -1
  32. package/dist/public/mini/zx.d.ts +2 -0
  33. package/dist/public/mini/zx.d.ts.map +1 -1
  34. package/dist/public/model.d.ts +6 -0
  35. package/dist/public/model.d.ts.map +1 -1
  36. package/dist/server/index.js +37 -27
  37. package/dist/server/index.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/internal/db.ts +33 -52
  40. package/src/internal/meta.ts +26 -0
  41. package/src/internal/model.ts +76 -9
  42. package/src/internal/zx.ts +18 -2
  43. package/src/public/cli/commands.ts +35 -6
  44. package/src/public/codegen/discover.ts +9 -1
  45. package/src/public/codegen/generate.ts +203 -15
  46. package/src/public/mini/model.ts +6 -0
  47. package/src/public/mini/zx.ts +3 -2
  48. package/src/public/model.ts +6 -0
package/dist/cli/index.js CHANGED
@@ -3,6 +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 { spawn } from 'child_process';
7
+ import { fileURLToPath } from 'url';
6
8
  import { z } from 'zod';
7
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';
8
10
  import 'convex/values';
@@ -318,7 +320,7 @@ function getMethodName(call) {
318
320
  if (expr.getKind() !== SyntaxKind.PropertyAccessExpression) return null;
319
321
  return expr.getName();
320
322
  }
321
- function transformWrappers(file) {
323
+ function transformWrappers(file, typeChecker) {
322
324
  let count = 0;
323
325
  const calls = file.getDescendantsOfKind(SyntaxKind.CallExpression).reverse();
324
326
  for (const call of calls) {
@@ -328,6 +330,18 @@ function transformWrappers(file) {
328
330
  if (call.getArguments().length > 0) continue;
329
331
  const obj = getCallObject(call);
330
332
  if (!obj) continue;
333
+ if (isNamespaceCall(obj)) continue;
334
+ let isSchema;
335
+ if (typeChecker) {
336
+ const typeResult = isZodSchemaByType(call, typeChecker);
337
+ isSchema = typeResult === true || typeResult === null && isLikelySchemaExpr(obj);
338
+ } else {
339
+ isSchema = isLikelySchemaExpr(obj);
340
+ }
341
+ if (!isSchema && isSchemaVariable(call, obj.trim())) {
342
+ isSchema = true;
343
+ }
344
+ if (!isSchema) continue;
331
345
  call.replaceWithText(`z.${method}(${obj})`);
332
346
  count++;
333
347
  }
@@ -337,8 +351,9 @@ function isNamespaceCall(obj) {
337
351
  return NAMESPACE_IDENTIFIERS.has(obj.trim());
338
352
  }
339
353
  function isLikelySchemaExpr(obj) {
340
- if (obj.match(/^z\.\w+\(/)) return true;
341
- if (obj.match(/^zx\.\w+\(/)) return true;
354
+ const normalized = obj.trim().replace(/^\(+/, "").replace(/\s+as\s+[\w$<>,\s|&[\]]+\)*\s*$/, "").trim();
355
+ if (normalized.match(/^z\.\w+\(/)) return true;
356
+ if (normalized.match(/^zx\.\w+\(/)) return true;
342
357
  return false;
343
358
  }
344
359
  function isSchemaVariable(call, varName) {
@@ -561,6 +576,17 @@ function findObjectOnlyMethods(file) {
561
576
  }
562
577
  return results;
563
578
  }
579
+ function ensureZImport(file) {
580
+ const text = file.getFullText();
581
+ if (!/\bz\./.test(text)) return;
582
+ const hasZImport = file.getImportDeclarations().some((imp) => imp.getNamedImports().some((n) => n.getName() === "z"));
583
+ if (hasZImport) return;
584
+ if (/^\s*(?:const|let|var)\s+z\s*[=:]/m.test(text)) return;
585
+ file.addImportDeclaration({
586
+ moduleSpecifier: "zod/mini",
587
+ namedImports: [{ name: "z" }]
588
+ });
589
+ }
564
590
  function transformImports(file) {
565
591
  let count = 0;
566
592
  const imports = file.getImportDeclarations();
@@ -708,7 +734,7 @@ function transformFile(file, typeChecker) {
708
734
  let propertyAccessors = 0;
709
735
  for (let i = 0; i < 10; i++) {
710
736
  const cr = transformConstructorReplacements(file);
711
- const w = transformWrappers(file);
737
+ const w = transformWrappers(file, typeChecker);
712
738
  const c = transformChecks(file);
713
739
  const m = transformMethods(file, typeChecker);
714
740
  const pa = transformPropertyAccessors(file, typeChecker);
@@ -722,6 +748,7 @@ function transformFile(file, typeChecker) {
722
748
  const classRefs = transformClassRefs(file);
723
749
  const objectOnlyWarnings = findObjectOnlyMethods(file);
724
750
  const propertyAccessWarnings = findInternalPropertyAccess(file, typeChecker);
751
+ ensureZImport(file);
725
752
  const imports = 0;
726
753
  return {
727
754
  filePath,
@@ -1002,6 +1029,20 @@ function readMeta(target) {
1002
1029
  }
1003
1030
  return target[META_KEY];
1004
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
+ }
1005
1046
  var metadata = /* @__PURE__ */ new WeakMap();
1006
1047
  var registryHelpers = {
1007
1048
  getMetadata: (type) => metadata.get(type),
@@ -1042,8 +1083,10 @@ function id(tableName) {
1042
1083
  branded._tableName = tableName;
1043
1084
  return branded;
1044
1085
  }
1045
- function codec(wire, runtime, transforms) {
1046
- 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;
1047
1090
  }
1048
1091
  function sharedWeakMap(name) {
1049
1092
  const key = /* @__PURE__ */ Symbol.for(`zodvex.zx.cache.${name}`);
@@ -1528,7 +1571,7 @@ async function discoverModules(convexDir2) {
1528
1571
  "crons.ts",
1529
1572
  "crons.js"
1530
1573
  ]
1531
- });
1574
+ }).sort();
1532
1575
  try {
1533
1576
  for (const file of files) {
1534
1577
  const absPath = path3.resolve(convexDir2, file);
@@ -1689,7 +1732,35 @@ var HEADER = `// AUTO-GENERATED by zodvex \u2014 do not edit
1689
1732
  `;
1690
1733
  function fingerprintCodec(schema) {
1691
1734
  if (!(schema instanceof $ZodCodec)) return "";
1692
- return `${zodToSource(schema._zod.def.in)}|${zodToSource(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}`;
1739
+ }
1740
+ function fingerprintLeaf(schema) {
1741
+ return `${zodToSource(schema)}#${fingerprintChecks(schema)}`;
1742
+ }
1743
+ function fingerprintChecks(schema) {
1744
+ const checks = schema?._zod?.def?.checks;
1745
+ if (!Array.isArray(checks) || checks.length === 0) return "";
1746
+ const parts = [];
1747
+ for (const check of checks) {
1748
+ const def = check?._zod?.def;
1749
+ if (!def) continue;
1750
+ const checkType = def.check ?? def.type ?? "check";
1751
+ const data = {};
1752
+ for (const [k, v7] of Object.entries(def)) {
1753
+ if (k === "check" || k === "type" || k === "error" || k === "message") continue;
1754
+ const t = typeof v7;
1755
+ if (t === "string" || t === "number" || t === "boolean" || v7 === null) {
1756
+ data[k] = v7;
1757
+ } else if (v7 instanceof RegExp) {
1758
+ data[k] = v7.source;
1759
+ }
1760
+ }
1761
+ parts.push(`${checkType}(${JSON.stringify(data)})`);
1762
+ }
1763
+ return parts.sort().join("&");
1693
1764
  }
1694
1765
  function generateSchemaFile(models) {
1695
1766
  const exports$1 = models.map((m) => {
@@ -1776,6 +1847,30 @@ function deriveCodecVarName(modelExportName, accessPath) {
1776
1847
  return `_${prefix}${fieldPart[0].toUpperCase() + fieldPart.slice(1)}`;
1777
1848
  }
1778
1849
  function generateApiFile(functions, models, codecs, modelCodecs, functionCodecs, options) {
1850
+ const sortedModels = [...models].sort(
1851
+ (a, b) => `${a.sourceFile}|${a.exportName}`.localeCompare(`${b.sourceFile}|${b.exportName}`)
1852
+ );
1853
+ const sortedFunctions = [...functions].sort(
1854
+ (a, b) => a.functionPath.localeCompare(b.functionPath)
1855
+ );
1856
+ const sortedCodecs = codecs ? [...codecs].sort(
1857
+ (a, b) => `${a.sourceFile}|${a.exportName}`.localeCompare(`${b.sourceFile}|${b.exportName}`)
1858
+ ) : void 0;
1859
+ const sortedModelCodecs = modelCodecs ? [...modelCodecs].sort(
1860
+ (a, b) => `${a.modelSourceFile}|${a.modelExportName}|${a.schemaKey}|${a.accessPath}`.localeCompare(
1861
+ `${b.modelSourceFile}|${b.modelExportName}|${b.schemaKey}|${b.accessPath}`
1862
+ )
1863
+ ) : void 0;
1864
+ const sortedFunctionCodecs = functionCodecs ? [...functionCodecs].sort(
1865
+ (a, b) => `${a.functionSourceFile}|${a.functionExportName}|${a.schemaSource}|${a.accessPath}`.localeCompare(
1866
+ `${b.functionSourceFile}|${b.functionExportName}|${b.schemaSource}|${b.accessPath}`
1867
+ )
1868
+ ) : void 0;
1869
+ models = sortedModels;
1870
+ functions = sortedFunctions;
1871
+ codecs = sortedCodecs;
1872
+ modelCodecs = sortedModelCodecs;
1873
+ functionCodecs = sortedFunctionCodecs;
1779
1874
  const identityMap = /* @__PURE__ */ new Map();
1780
1875
  const neededModelImports = /* @__PURE__ */ new Set();
1781
1876
  for (const model of models) {
@@ -1859,21 +1954,74 @@ function generateApiFile(functions, models, codecs, modelCodecs, functionCodecs,
1859
1954
  }
1860
1955
  if (functionCodecs) {
1861
1956
  const fingerprintMap = /* @__PURE__ */ new Map();
1957
+ const brandMap = /* @__PURE__ */ new Map();
1958
+ const codecSchemaToSourceFile = /* @__PURE__ */ new Map();
1959
+ for (const c of codecs ?? []) {
1960
+ codecSchemaToSourceFile.set(c.schema, c.sourceFile);
1961
+ }
1962
+ for (const mc of modelCodecs ?? []) {
1963
+ codecSchemaToSourceFile.set(mc.codec, mc.modelSourceFile);
1964
+ }
1862
1965
  for (const [codecSchema, ref] of codecMap) {
1966
+ const sourceFile = codecSchemaToSourceFile.get(codecSchema);
1967
+ const brand = readCodecBrand(codecSchema);
1968
+ const candidate = { ref, sourceFile, brand };
1863
1969
  const fp = fingerprintCodec(codecSchema);
1864
- if (fp) fingerprintMap.set(fp, ref);
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]);
1979
+ }
1865
1980
  }
1981
+ const undiscoverable = [];
1866
1982
  for (const fc of functionCodecs) {
1867
1983
  if (codecMap.has(fc.codec)) continue;
1868
- const fp = fingerprintCodec(fc.codec);
1869
- const matchingRef = fp ? fingerprintMap.get(fp) : void 0;
1870
- if (matchingRef) {
1871
- codecMap.set(fc.codec, matchingRef);
1872
- } else {
1873
- console.warn(
1874
- `[zodvex] Warning: Codec in ${fc.functionExportName}() (${fc.accessPath}) has no matching model or exported codec. Export it standalone for full client-side codec support.`
1984
+ const brand = readCodecBrand(fc.codec);
1985
+ let candidates = brand ? brandMap.get(brand) : void 0;
1986
+ if (!candidates || candidates.length === 0) {
1987
+ const fp = fingerprintCodec(fc.codec);
1988
+ candidates = (fp ? fingerprintMap.get(fp) : void 0)?.filter(
1989
+ (c) => c.brand === void 0 || c.brand === brand
1875
1990
  );
1876
1991
  }
1992
+ if (!candidates || candidates.length === 0) {
1993
+ undiscoverable.push({ fn: fc.functionExportName, path: fc.accessPath });
1994
+ continue;
1995
+ }
1996
+ let chosen;
1997
+ if (candidates.length === 1) {
1998
+ chosen = candidates[0].ref;
1999
+ } else {
2000
+ const sameFile = candidates.filter((c) => c.sourceFile === fc.functionSourceFile);
2001
+ if (sameFile.length === 1) {
2002
+ chosen = sameFile[0].ref;
2003
+ } else {
2004
+ const sorted = [...candidates].sort(
2005
+ (a, b) => `${a.sourceFile ?? ""}|${a.ref.exportName}`.localeCompare(
2006
+ `${b.sourceFile ?? ""}|${b.ref.exportName}`
2007
+ )
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
+ }
2015
+ }
2016
+ }
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
+ );
1877
2025
  }
1878
2026
  }
1879
2027
  const zodToSourceCtx = {
@@ -1932,15 +2080,17 @@ function generateApiFile(functions, models, codecs, modelCodecs, functionCodecs,
1932
2080
  if (coreImports.length > 0) {
1933
2081
  imports.push(`import { ${coreImports.join(", ")} } from '${zodvexImport}'`);
1934
2082
  }
1935
- for (const exportName of neededModelImports) {
2083
+ for (const exportName of [...neededModelImports].sort()) {
1936
2084
  const model = models.find((m) => m.exportName === exportName);
1937
2085
  if (model) {
1938
2086
  const importPath = `../${model.sourceFile.replace(/\.ts$/, ".js")}`;
1939
2087
  imports.push(`import { ${exportName} } from '${importPath}'`);
1940
2088
  }
1941
2089
  }
1942
- for (const [importPath, exportNames] of zodToSourceCtx.neededCodecImports) {
2090
+ const sortedCodecImportPaths = [...zodToSourceCtx.neededCodecImports.keys()].sort();
2091
+ for (const importPath of sortedCodecImportPaths) {
1943
2092
  if (importPath === MODEL_CODEC_SENTINEL) continue;
2093
+ const exportNames = zodToSourceCtx.neededCodecImports.get(importPath) ?? /* @__PURE__ */ new Set();
1944
2094
  const names = Array.from(exportNames).sort().join(", ");
1945
2095
  imports.push(`import { ${names} } from '${importPath}'`);
1946
2096
  }
@@ -2080,13 +2230,9 @@ async function dev(convexDir2, options) {
2080
2230
  return;
2081
2231
  }
2082
2232
  if (debounceTimer) clearTimeout(debounceTimer);
2083
- debounceTimer = setTimeout(async () => {
2233
+ debounceTimer = setTimeout(() => {
2084
2234
  console.log("[zodvex] Regenerating...");
2085
- try {
2086
- await generate(resolved, options);
2087
- } catch (err) {
2088
- console.error("[zodvex] Generation failed:", err.message);
2089
- }
2235
+ void regenerate(resolved, options);
2090
2236
  }, 300);
2091
2237
  });
2092
2238
  process.on("SIGINT", () => {
@@ -2095,6 +2241,22 @@ async function dev(convexDir2, options) {
2095
2241
  process.exit(0);
2096
2242
  });
2097
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
+ }
2098
2260
  function writeStubApi(zodvexDir) {
2099
2261
  fs2.mkdirSync(zodvexDir, { recursive: true });
2100
2262
  fs2.writeFileSync(