UNPKG

prisma-zod-generator

Version:

Prisma 2+ generator to emit Zod schemas from your Prisma schema

319 lines 13.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.setSingleFilePrismaImportPath = setSingleFilePrismaImportPath; exports.initSingleFile = initSingleFile; exports.isSingleFileEnabled = isSingleFileEnabled; exports.appendSingleFile = appendSingleFile; exports.flushSingleFile = flushSingleFile; const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); let enabled = false; let bundlePath = ''; let chunks = []; let needsZodImport = false; let needsPrismaTypeImport = false; // import type { Prisma } let needsPrismaValueImport = false; // import { Prisma } (as value) const prismaValueImports = new Set(); // enums etc. let sawPrismaAlias = false; // whether __PrismaAlias was referenced let prismaImportBase = '@prisma/client'; let needsJsonHelpers = false; // whether to inject json helpers block const exportedTypeNames = new Set(); // track exported type identifiers to prevent collisions function setSingleFilePrismaImportPath(importPath, extension) { let finalPath = (importPath || '@prisma/client').replace(/\\/g, '/'); if (extension && !finalPath.endsWith(extension) && !finalPath.includes('node_modules')) { finalPath += extension; } prismaImportBase = finalPath; } function initSingleFile(bundleFullPath) { enabled = true; bundlePath = bundleFullPath; chunks = []; needsZodImport = false; needsPrismaTypeImport = false; needsPrismaValueImport = false; prismaValueImports.clear(); sawPrismaAlias = false; needsJsonHelpers = false; exportedTypeNames.clear(); } function isSingleFileEnabled() { return enabled; } function appendSingleFile(filePath, rawContent) { if (!enabled) return; // Strip imports and rename conflicting local identifiers const content = transformContentForSingleFile(filePath, rawContent); chunks.push({ filePath, content }); } function transformContentForSingleFile(filePath, source) { const lines = source.split(/\r?\n/); const kept = []; let inJsonSkip = false; const relImportRe = /^\s*import\s+[^'";]+from\s+['"](\.\.?\/)[^'"]+['"];?\s*$/; // Detect Zod import variant (zod or zod/v4) importing z const zodImportRe = /^\s*import\s+(?:\{\s*z\s*\}|\*\s+as\s+z)\s+from\s+['"](?:zod(?:\/v4)?)['"];?\s*$/; const escapedPrismaImport = escapeRegExp(prismaImportBase); const prismaTypeImportRe = new RegExp(`^\\s*import\\s+type\\s+\\{\\s*Prisma\\s*\\}\\s+from\\s+['\"]${escapedPrismaImport}['\"];?\\s*$`); const prismaValueImportRe = new RegExp(`^\\s*import\\s+\\{\\s*([^}]+)\\s*\\}\\s+from\\s+['\"]${escapedPrismaImport}['\"];?\\s*$`); const prismaAliasTypeRe = /^\s*type\s+__PrismaAlias\s*=\s*Prisma\./; // Relative re-exports don't make sense in a single bundled file const relExportStarRe = /^\s*export\s+\*\s+from\s+['"](\.\.?\/)[^'"]+['"];?\s*$/; const relExportNamesRe = /^\s*export\s+\{[^}]+\}\s+from\s+['"](\.\.?\/)[^'"]+['"];?\s*$/; // We'll collect lines then fix the common local "const Schema" alias to a unique name for (const line of lines) { // Detect and strip inline JSON helper blocks (comment + IIFE) keeping only a single hoisted version if (/JSON helper schemas/.test(line)) { needsJsonHelpers = true; // Skip this marker line and enter skip mode until block end inJsonSkip = true; continue; } if (typeof inJsonSkip !== 'undefined' && inJsonSkip) { // Block ends when we hit the closing IIFE line '})();' if (/^\s*\)\(\);\s*$/.test(line) || /^\s*\)\);\s*$/.test(line)) { inJsonSkip = false; // finished skipping block } continue; // skip all lines within block } if (/import\s+\{\s*JsonValueSchema\s+as\s+jsonSchema\s*\}\s+from\s+['"](?:\.{1,2}\/)+helpers\/json-helpers(?:\.js)?['"];?/.test(line)) { needsJsonHelpers = true; continue; } if (zodImportRe.test(line)) { needsZodImport = true; continue; } if (prismaTypeImportRe.test(line)) { needsPrismaTypeImport = true; continue; } const m = line.match(prismaValueImportRe); if (m) { m[1] .split(',') .map((s) => s.trim()) .filter(Boolean) .forEach((name) => { // Don't add Prisma to value imports if it's used as a type import if (name !== 'Prisma') { prismaValueImports.add(name); } }); continue; } if (relImportRe.test(line)) { continue; } if (relExportStarRe.test(line)) { continue; } if (relExportNamesRe.test(line)) { continue; } if (prismaAliasTypeRe.test(line)) { sawPrismaAlias = true; continue; } kept.push(line); } let text = kept.join('\n'); // If file uses the pattern: const Schema = ... ; export const XObjectSchema = Schema // rename local Schema to a unique identifier, and reference it in the export line const base = path_1.default .basename(filePath) .replace(/\.[jt]s$/, '') .replace(/[^a-zA-Z0-9_]/g, '_'); const unique = `__Schema_${base}`; const uniqueMakeSchema = `__makeSchema_${base}`; // Replace only the first "const Schema[ :type]? =" per file, preserving any type annotation text = text.replace(/(^|\n)\s*const\s+Schema(\s*:\s*[^=]+)?\s*=\s*/m, (_m, p1, typeAnn = '') => `${p1}const ${unique}${typeAnn} = `); text = text.replace(/export\s+const\s+(\w+)ObjectSchema\s*=\s*Schema/g, `export const $1ObjectSchema = ${unique}`); // Handle makeSchema function declarations - rename to unique identifier text = text.replace(/(^|\n)\s*const\s+makeSchema\s*=\s*/m, (_m, p1) => `${p1}const ${uniqueMakeSchema} = `); // Replace all references to makeSchema with the unique name text = text.replace(/\bmakeSchema\b/g, uniqueMakeSchema); // Uniquify duplicate SelectSchema identifiers that appear across different files const selectDeclRe = /export\s+const\s+([A-Za-z0-9_]+SelectSchema)\b/g; const selectZodDeclRe = /export\s+const\s+([A-Za-z0-9_]+SelectZodSchema)\b/g; const renameMap = new Map(); const suffix = `__${base}`; for (const re of [selectDeclRe, selectZodDeclRe]) { re.lastIndex = 0; let m2; while ((m2 = re.exec(text)) !== null) { const orig = m2[1]; if (!renameMap.has(orig)) renameMap.set(orig, `${orig}${suffix}`); } } if (renameMap.size > 0) { for (const [orig, renamed] of renameMap) { const idRe = new RegExp(`\\b${orig}\\b`, 'g'); text = text.replace(idRe, renamed); } } // Heuristic: if native enums are referenced (e.g., z.enum(Role) or z.nativeEnum(Role)), // hoist those enum names as value imports from @prisma/client const enumUseRe = /z\.(?:enum|nativeEnum)\(([_A-Za-z][_A-Za-z0-9]*)\)/g; let em; enumUseRe.lastIndex = 0; while ((em = enumUseRe.exec(text)) !== null) { const name = em[1]; // Avoid picking up local Schema identifiers if (name && !name.endsWith('Schema')) prismaValueImports.add(name); } // Check for actual Prisma value usage (not type positions) // Look for patterns like z.nativeEnum(Prisma.Something) but exclude type annotations const prismaValueUseRe = /z\.(enum|nativeEnum)\(Prisma\.([A-Za-z][A-Za-z0-9]*)\)/g; if (prismaValueUseRe.test(text)) { needsPrismaValueImport = true; } // Check for Prisma type usage in ZodType generics like z.ZodType<Prisma.Something> const prismaTypeUseRe = /z\.ZodType<Prisma\.([A-Za-z][A-Za-z0-9]*)/g; if (prismaTypeUseRe.test(text)) { needsPrismaTypeImport = true; } text = dedupeExportTypeNames(text, filePath); return `// File: ${path_1.default.basename(filePath)}\n${text}\n`; } function dedupeExportTypeNames(text, filePath) { const typeExportRe = /export\s+type\s+([A-Za-z0-9_]+)\s*=\s*z\.infer<typeof\s+([A-Za-z0-9_]+)\s*>;\s*/g; return text.replace(typeExportRe, (match, typeName, schemaRef) => { const finalName = reserveExportTypeName(typeName, schemaRef, filePath); if (finalName === typeName) { return match; } return `export type ${finalName} = z.infer<typeof ${schemaRef}>;`; }); } function reserveExportTypeName(typeName, schemaRef, filePath) { if (!exportedTypeNames.has(typeName)) { exportedTypeNames.add(typeName); return typeName; } const candidates = []; if (typeName.endsWith('Type')) { candidates.push(`${typeName.slice(0, -4)}Model`); } if (schemaRef.endsWith('Schema')) { const base = schemaRef.slice(0, -'Schema'.length); candidates.push(`${base}Model`); candidates.push(`${base}SchemaType`); } const baseName = path_1.default .basename(filePath) .replace(/\.[jt]s$/, '') .replace(/[^A-Za-z0-9_]/g, ''); if (baseName) { candidates.push(`${baseName}Model`); } candidates.push(`${typeName}Model`); for (const candidate of candidates) { const sanitized = candidate.replace(/[^A-Za-z0-9_]/g, '_'); if (sanitized && !exportedTypeNames.has(sanitized)) { exportedTypeNames.add(sanitized); return sanitized; } } let counter = 2; while (true) { const fallback = `${typeName}${counter}`; if (!exportedTypeNames.has(fallback)) { exportedTypeNames.add(fallback); return fallback; } counter += 1; } } async function flushSingleFile() { var _a; if (!enabled || !bundlePath) return; const header = [ '/**', ' * Prisma Zod Generator - Single File (inlined)', ' * Auto-generated. Do not edit.', ' */', '', ]; if (needsZodImport) { // Dynamically resolve Zod import based on generator config try { // eslint-disable-next-line @typescript-eslint/no-require-imports const transformer = require('../transformer').default; const zImport = ((_a = transformer === null || transformer === void 0 ? void 0 : transformer.prototype) === null || _a === void 0 ? void 0 : _a.generateImportZodStatement) ? transformer.prototype.generateImportZodStatement.call(transformer) : "import * as z from 'zod';\n"; header.push(zImport.trim()); } catch { header.push(`import * as z from 'zod';`); } } // Handle Prisma imports - if we need both type and value imports, use separate lines if (needsPrismaTypeImport && needsPrismaValueImport) { header.push(`import type { Prisma } from '${prismaImportBase}';`); header.push(`import { Prisma } from '${prismaImportBase}';`); } else if (needsPrismaTypeImport) { header.push(`import type { Prisma } from '${prismaImportBase}';`); } else if (needsPrismaValueImport) { header.push(`import { Prisma } from '${prismaImportBase}';`); } if (prismaValueImports.size > 0) { // Don't duplicate Prisma if it's already imported above const valueImports = Array.from(prismaValueImports) .filter((name) => name !== 'Prisma') .sort(); if (valueImports.length > 0) { header.push(`import { ${valueImports.join(', ')} } from '${prismaImportBase}';`); } } if (needsJsonHelpers) { header.push(`// JSON helper schemas (hoisted)`); header.push(`const jsonSchema = (() => {`); header.push(` const JsonValueSchema: any = (() => {`); header.push(` const recur: any = z.lazy(() => z.union([`); header.push(` z.string(), z.number(), z.boolean(), z.literal(null),`); header.push(` z.record(z.string(), z.lazy(() => recur.optional())),`); header.push(` z.array(z.lazy(() => recur)),`); header.push(` ]));`); header.push(` return recur;`); header.push(` })();`); header.push(` return JsonValueSchema;`); header.push(`})();`); } if (sawPrismaAlias) { header.push(`type __PrismaAlias = Prisma.JsonValue | Prisma.InputJsonValue;`); // Ensure Prisma type import is present if (!needsPrismaTypeImport && !needsPrismaValueImport) { header.unshift(`import type { Prisma } from '${prismaImportBase}';`); needsPrismaTypeImport = true; } } header.push(''); const dir = path_1.default.dirname(bundlePath); await fs_1.promises.mkdir(dir, { recursive: true }); const body = chunks.map((c) => c.content).join('\n'); await fs_1.promises.writeFile(bundlePath, header.join('\n') + body, 'utf8'); // Reset state after writing enabled = false; bundlePath = ''; chunks = []; needsZodImport = false; needsPrismaTypeImport = false; needsPrismaValueImport = false; prismaValueImports.clear(); sawPrismaAlias = false; needsJsonHelpers = false; exportedTypeNames.clear(); } //# sourceMappingURL=singleFileAggregator.js.map