zodvex 0.7.1 → 0.7.2-beta.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.
package/dist/cli/index.js CHANGED
@@ -3,6 +3,7 @@ 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
7
  import { z } from 'zod';
7
8
  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
9
  import 'convex/values';
@@ -318,7 +319,7 @@ function getMethodName(call) {
318
319
  if (expr.getKind() !== SyntaxKind.PropertyAccessExpression) return null;
319
320
  return expr.getName();
320
321
  }
321
- function transformWrappers(file) {
322
+ function transformWrappers(file, typeChecker) {
322
323
  let count = 0;
323
324
  const calls = file.getDescendantsOfKind(SyntaxKind.CallExpression).reverse();
324
325
  for (const call of calls) {
@@ -328,6 +329,18 @@ function transformWrappers(file) {
328
329
  if (call.getArguments().length > 0) continue;
329
330
  const obj = getCallObject(call);
330
331
  if (!obj) continue;
332
+ if (isNamespaceCall(obj)) continue;
333
+ let isSchema;
334
+ if (typeChecker) {
335
+ const typeResult = isZodSchemaByType(call, typeChecker);
336
+ isSchema = typeResult === true || typeResult === null && isLikelySchemaExpr(obj);
337
+ } else {
338
+ isSchema = isLikelySchemaExpr(obj);
339
+ }
340
+ if (!isSchema && isSchemaVariable(call, obj.trim())) {
341
+ isSchema = true;
342
+ }
343
+ if (!isSchema) continue;
331
344
  call.replaceWithText(`z.${method}(${obj})`);
332
345
  count++;
333
346
  }
@@ -337,8 +350,9 @@ function isNamespaceCall(obj) {
337
350
  return NAMESPACE_IDENTIFIERS.has(obj.trim());
338
351
  }
339
352
  function isLikelySchemaExpr(obj) {
340
- if (obj.match(/^z\.\w+\(/)) return true;
341
- if (obj.match(/^zx\.\w+\(/)) return true;
353
+ const normalized = obj.trim().replace(/^\(+/, "").replace(/\s+as\s+[\w$<>,\s|&[\]]+\)*\s*$/, "").trim();
354
+ if (normalized.match(/^z\.\w+\(/)) return true;
355
+ if (normalized.match(/^zx\.\w+\(/)) return true;
342
356
  return false;
343
357
  }
344
358
  function isSchemaVariable(call, varName) {
@@ -561,6 +575,17 @@ function findObjectOnlyMethods(file) {
561
575
  }
562
576
  return results;
563
577
  }
578
+ function ensureZImport(file) {
579
+ const text = file.getFullText();
580
+ if (!/\bz\./.test(text)) return;
581
+ const hasZImport = file.getImportDeclarations().some((imp) => imp.getNamedImports().some((n) => n.getName() === "z"));
582
+ if (hasZImport) return;
583
+ if (/^\s*(?:const|let|var)\s+z\s*[=:]/m.test(text)) return;
584
+ file.addImportDeclaration({
585
+ moduleSpecifier: "zod/mini",
586
+ namedImports: [{ name: "z" }]
587
+ });
588
+ }
564
589
  function transformImports(file) {
565
590
  let count = 0;
566
591
  const imports = file.getImportDeclarations();
@@ -708,7 +733,7 @@ function transformFile(file, typeChecker) {
708
733
  let propertyAccessors = 0;
709
734
  for (let i = 0; i < 10; i++) {
710
735
  const cr = transformConstructorReplacements(file);
711
- const w = transformWrappers(file);
736
+ const w = transformWrappers(file, typeChecker);
712
737
  const c = transformChecks(file);
713
738
  const m = transformMethods(file, typeChecker);
714
739
  const pa = transformPropertyAccessors(file, typeChecker);
@@ -722,6 +747,7 @@ function transformFile(file, typeChecker) {
722
747
  const classRefs = transformClassRefs(file);
723
748
  const objectOnlyWarnings = findObjectOnlyMethods(file);
724
749
  const propertyAccessWarnings = findInternalPropertyAccess(file, typeChecker);
750
+ ensureZImport(file);
725
751
  const imports = 0;
726
752
  return {
727
753
  filePath,
@@ -1368,6 +1394,7 @@ function findCodec(schema) {
1368
1394
  }
1369
1395
 
1370
1396
  // src/public/codegen/discover.ts
1397
+ var discoveryRunCounter = 0;
1371
1398
  function walkSchemaRecursive(schema, accessPath, visited, seenCodecs, results) {
1372
1399
  if (visited.has(schema)) return;
1373
1400
  visited.add(schema);
@@ -1505,7 +1532,7 @@ function walkFunctionCodecs(functions) {
1505
1532
  }
1506
1533
  return found;
1507
1534
  }
1508
- async function discoverModules(convexDir2) {
1535
+ async function discoverModules(convexDir2, options) {
1509
1536
  const models = [];
1510
1537
  const functions = [];
1511
1538
  const codecs = [];
@@ -1528,13 +1555,19 @@ async function discoverModules(convexDir2) {
1528
1555
  "crons.ts",
1529
1556
  "crons.js"
1530
1557
  ]
1531
- });
1558
+ }).sort();
1559
+ let cacheKey = null;
1560
+ if (options?.freshImports) {
1561
+ discoveryRunCounter += 1;
1562
+ cacheKey = `${process.pid}-${discoveryRunCounter}`;
1563
+ }
1532
1564
  try {
1533
1565
  for (const file of files) {
1534
1566
  const absPath = path3.resolve(convexDir2, file);
1567
+ const importUrl = cacheKey ? `${pathToFileURL(absPath).href}?t=${cacheKey}` : absPath;
1535
1568
  let moduleExports;
1536
1569
  try {
1537
- moduleExports = await import(absPath);
1570
+ moduleExports = await import(importUrl);
1538
1571
  } catch (err) {
1539
1572
  console.warn(`[zodvex] Warning: Failed to import ${file}:`, err.message);
1540
1573
  continue;
@@ -1689,7 +1722,32 @@ var HEADER = `// AUTO-GENERATED by zodvex \u2014 do not edit
1689
1722
  `;
1690
1723
  function fingerprintCodec(schema) {
1691
1724
  if (!(schema instanceof $ZodCodec)) return "";
1692
- return `${zodToSource(schema._zod.def.in)}|${zodToSource(schema._zod.def.out)}`;
1725
+ return `${fingerprintLeaf(schema._zod.def.in)}|${fingerprintLeaf(schema._zod.def.out)}`;
1726
+ }
1727
+ function fingerprintLeaf(schema) {
1728
+ return `${zodToSource(schema)}#${fingerprintChecks(schema)}`;
1729
+ }
1730
+ function fingerprintChecks(schema) {
1731
+ const checks = schema?._zod?.def?.checks;
1732
+ if (!Array.isArray(checks) || checks.length === 0) return "";
1733
+ const parts = [];
1734
+ for (const check of checks) {
1735
+ const def = check?._zod?.def;
1736
+ if (!def) continue;
1737
+ const checkType = def.check ?? def.type ?? "check";
1738
+ const data = {};
1739
+ for (const [k, v7] of Object.entries(def)) {
1740
+ if (k === "check" || k === "type" || k === "error" || k === "message") continue;
1741
+ const t = typeof v7;
1742
+ if (t === "string" || t === "number" || t === "boolean" || v7 === null) {
1743
+ data[k] = v7;
1744
+ } else if (v7 instanceof RegExp) {
1745
+ data[k] = v7.source;
1746
+ }
1747
+ }
1748
+ parts.push(`${checkType}(${JSON.stringify(data)})`);
1749
+ }
1750
+ return parts.sort().join("&");
1693
1751
  }
1694
1752
  function generateSchemaFile(models) {
1695
1753
  const exports$1 = models.map((m) => {
@@ -1776,6 +1834,30 @@ function deriveCodecVarName(modelExportName, accessPath) {
1776
1834
  return `_${prefix}${fieldPart[0].toUpperCase() + fieldPart.slice(1)}`;
1777
1835
  }
1778
1836
  function generateApiFile(functions, models, codecs, modelCodecs, functionCodecs, options) {
1837
+ const sortedModels = [...models].sort(
1838
+ (a, b) => `${a.sourceFile}|${a.exportName}`.localeCompare(`${b.sourceFile}|${b.exportName}`)
1839
+ );
1840
+ const sortedFunctions = [...functions].sort(
1841
+ (a, b) => a.functionPath.localeCompare(b.functionPath)
1842
+ );
1843
+ const sortedCodecs = codecs ? [...codecs].sort(
1844
+ (a, b) => `${a.sourceFile}|${a.exportName}`.localeCompare(`${b.sourceFile}|${b.exportName}`)
1845
+ ) : void 0;
1846
+ const sortedModelCodecs = modelCodecs ? [...modelCodecs].sort(
1847
+ (a, b) => `${a.modelSourceFile}|${a.modelExportName}|${a.schemaKey}|${a.accessPath}`.localeCompare(
1848
+ `${b.modelSourceFile}|${b.modelExportName}|${b.schemaKey}|${b.accessPath}`
1849
+ )
1850
+ ) : void 0;
1851
+ const sortedFunctionCodecs = functionCodecs ? [...functionCodecs].sort(
1852
+ (a, b) => `${a.functionSourceFile}|${a.functionExportName}|${a.schemaSource}|${a.accessPath}`.localeCompare(
1853
+ `${b.functionSourceFile}|${b.functionExportName}|${b.schemaSource}|${b.accessPath}`
1854
+ )
1855
+ ) : void 0;
1856
+ models = sortedModels;
1857
+ functions = sortedFunctions;
1858
+ codecs = sortedCodecs;
1859
+ modelCodecs = sortedModelCodecs;
1860
+ functionCodecs = sortedFunctionCodecs;
1779
1861
  const identityMap = /* @__PURE__ */ new Map();
1780
1862
  const neededModelImports = /* @__PURE__ */ new Set();
1781
1863
  for (const model of models) {
@@ -1859,20 +1941,49 @@ function generateApiFile(functions, models, codecs, modelCodecs, functionCodecs,
1859
1941
  }
1860
1942
  if (functionCodecs) {
1861
1943
  const fingerprintMap = /* @__PURE__ */ new Map();
1944
+ const codecSchemaToSourceFile = /* @__PURE__ */ new Map();
1945
+ for (const c of codecs ?? []) {
1946
+ codecSchemaToSourceFile.set(c.schema, c.sourceFile);
1947
+ }
1948
+ for (const mc of modelCodecs ?? []) {
1949
+ codecSchemaToSourceFile.set(mc.codec, mc.modelSourceFile);
1950
+ }
1862
1951
  for (const [codecSchema, ref] of codecMap) {
1863
1952
  const fp = fingerprintCodec(codecSchema);
1864
- if (fp) fingerprintMap.set(fp, ref);
1953
+ if (!fp) continue;
1954
+ 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 }]);
1960
+ }
1865
1961
  }
1866
1962
  for (const fc of functionCodecs) {
1867
1963
  if (codecMap.has(fc.codec)) continue;
1868
1964
  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 {
1965
+ const candidates = fp ? fingerprintMap.get(fp) : void 0;
1966
+ if (!candidates || candidates.length === 0) {
1873
1967
  console.warn(
1874
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.`
1875
1969
  );
1970
+ continue;
1971
+ }
1972
+ let chosen;
1973
+ if (candidates.length === 1) {
1974
+ chosen = candidates[0].ref;
1975
+ } else {
1976
+ const sameFile = candidates.filter((c) => c.sourceFile === fc.functionSourceFile);
1977
+ if (sameFile.length === 1) {
1978
+ chosen = sameFile[0].ref;
1979
+ } 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.`
1982
+ );
1983
+ }
1984
+ }
1985
+ if (chosen) {
1986
+ codecMap.set(fc.codec, chosen);
1876
1987
  }
1877
1988
  }
1878
1989
  }
@@ -1932,15 +2043,17 @@ function generateApiFile(functions, models, codecs, modelCodecs, functionCodecs,
1932
2043
  if (coreImports.length > 0) {
1933
2044
  imports.push(`import { ${coreImports.join(", ")} } from '${zodvexImport}'`);
1934
2045
  }
1935
- for (const exportName of neededModelImports) {
2046
+ for (const exportName of [...neededModelImports].sort()) {
1936
2047
  const model = models.find((m) => m.exportName === exportName);
1937
2048
  if (model) {
1938
2049
  const importPath = `../${model.sourceFile.replace(/\.ts$/, ".js")}`;
1939
2050
  imports.push(`import { ${exportName} } from '${importPath}'`);
1940
2051
  }
1941
2052
  }
1942
- for (const [importPath, exportNames] of zodToSourceCtx.neededCodecImports) {
2053
+ const sortedCodecImportPaths = [...zodToSourceCtx.neededCodecImports.keys()].sort();
2054
+ for (const importPath of sortedCodecImportPaths) {
1943
2055
  if (importPath === MODEL_CODEC_SENTINEL) continue;
2056
+ const exportNames = zodToSourceCtx.neededCodecImports.get(importPath) ?? /* @__PURE__ */ new Set();
1944
2057
  const names = Array.from(exportNames).sort().join(", ");
1945
2058
  imports.push(`import { ${names} } from '${importPath}'`);
1946
2059
  }
@@ -2043,7 +2156,7 @@ async function generate(convexDir2, options) {
2043
2156
  const resolved = resolveConvexDir(convexDir2);
2044
2157
  const zodvexDir = path3.join(resolved, "_zodvex");
2045
2158
  writeStubApi(zodvexDir);
2046
- const result = await discoverModules(resolved);
2159
+ const result = await discoverModules(resolved, { freshImports: options?.freshImports });
2047
2160
  const schemaContent = generateSchemaFile(result.models);
2048
2161
  const apiContent = generateApiFile(
2049
2162
  result.functions,
@@ -2083,7 +2196,7 @@ async function dev(convexDir2, options) {
2083
2196
  debounceTimer = setTimeout(async () => {
2084
2197
  console.log("[zodvex] Regenerating...");
2085
2198
  try {
2086
- await generate(resolved, options);
2199
+ await generate(resolved, { ...options, freshImports: true });
2087
2200
  } catch (err) {
2088
2201
  console.error("[zodvex] Generation failed:", err.message);
2089
2202
  }