prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
319 lines • 13.7 kB
JavaScript
;
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