@drzl/cli 4.0.0 → 4.2.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.
@@ -53,6 +53,18 @@ var GeneratorSchema = z.object({
53
53
  importExtension: ImportExtensionSchema.optional(),
54
54
  template: z.string().optional(),
55
55
  includeRelations: z.boolean().optional(),
56
+ /**
57
+ * Type `json` and `jsonb` columns from the schema rather than leaving them wide.
58
+ *
59
+ * `.$type<T>()` is a compile-time cast, so no runtime-derived validator can see it and
60
+ * `drizzle-orm/zod` types every json column as its generic `Json`. A generator can reference
61
+ * `typeof <table>.$inferSelect['<column>']` instead, which is the declared type resolved by
62
+ * TypeScript itself, so generics, unions and imported interfaces all work.
63
+ *
64
+ * Off by default because it makes the generated file import your schema module, as a
65
+ * type-only import that disappears at build time.
66
+ */
67
+ typedJson: z.boolean().optional(),
56
68
  naming: NamingSchema.optional(),
57
69
  outputHeader: z.object({
58
70
  enabled: z.boolean().default(true).optional(),
@@ -99,6 +111,24 @@ var AnalyzerSchema = z.object({
99
111
  var ConfigSchema = z.object({
100
112
  schema: z.string(),
101
113
  outDir: z.string().default("src/api"),
114
+ /**
115
+ * Which tables to generate for, matched against the database table name.
116
+ *
117
+ * There was no way to say this, and every generator loops over every table it finds, so
118
+ * DRZL emitted unauthenticated CRUD over whatever shared the schema file. That is noise for
119
+ * a migrations table and a genuine leak for an auth one: Better Auth puts `user`, `session`,
120
+ * `account` and `verification` alongside your own tables, and `account` holds
121
+ * `accessToken`, `refreshToken`, `idToken` and `password`.
122
+ *
123
+ * Deliberately name-based and explicit rather than detecting any particular library. Auth
124
+ * table names are all renameable, so a built-in list would miss renamed tables and, worse,
125
+ * silently skip an ordinary table that happened to be called `user`, which is usually the
126
+ * application's main entity.
127
+ *
128
+ * `exclude` wins over `include`. Patterns support `*`, matching within a name.
129
+ */
130
+ include: z.array(z.string()).optional(),
131
+ exclude: z.array(z.string()).optional(),
102
132
  /**
103
133
  * How every relative specifier drzl invents spells its extension, for every generator.
104
134
  * A generator may override it. Defaults to `js`, which is the only form that resolves
@@ -270,6 +300,16 @@ function resolveTemplateDirsSync(cfg, cwd = process.cwd()) {
270
300
  }
271
301
  return Array.from(new Set(results));
272
302
  }
303
+ function filterTables(tables, opts) {
304
+ const toRegExp = (pattern) => new RegExp(
305
+ "^" + pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
306
+ );
307
+ const matches = (patterns, name) => patterns.some((p) => toRegExp(p).test(name));
308
+ let out = tables;
309
+ if (opts.include?.length) out = out.filter((t) => matches(opts.include, t.name));
310
+ if (opts.exclude?.length) out = out.filter((t) => !matches(opts.exclude, t.name));
311
+ return out;
312
+ }
273
313
  function computeWatchTargets(cfg, cwd = process.cwd()) {
274
314
  const abs = (p) => path.resolve(cwd, p);
275
315
  const schemaAbs = abs(cfg.schema);
@@ -296,6 +336,7 @@ export {
296
336
  loadConfig,
297
337
  computeGeneratorOutputDirs,
298
338
  resolveTemplateDirsSync,
339
+ filterTables,
299
340
  computeWatchTargets
300
341
  };
301
- //# sourceMappingURL=chunk-VSEU5SZ6.js.map
342
+ //# sourceMappingURL=chunk-UXA4IRLP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import type { AffixOptions } from '@drzl/validation-core';\nimport {\n AFFIX_PROBE_TABLE,\n DEFAULT_IMPORT_EXTENSION,\n IMPORT_EXTENSIONS,\n NAME_MODES,\n resolveAffix,\n schemaName,\n validateAffix,\n} from '@drzl/validation-core';\nimport * as fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\nexport const NamingSchema = z\n .object({\n routerSuffix: z.string().default('Router'),\n procedureCase: z.enum(['camel', 'kebab', 'snake']).default('camel'),\n })\n .partial();\n\n/** One affix for every mode, or a per-mode map. Keys match drzl's internal mode names. */\nconst AffixValueSchema = z.union(\n [\n z.string(),\n z\n .object({\n insert: z.string().optional(),\n update: z.string().optional(),\n select: z.string().optional(),\n })\n .strict(),\n ],\n {\n error:\n 'Expected a string to use for every mode, or an object with any of the keys \"insert\", ' +\n '\"update\" and \"select\". Those keys are lowercase, matching the mode names drzl uses ' +\n 'everywhere else.',\n }\n);\n\nconst AffixPartSchema = z\n .object({\n prefix: AffixValueSchema.optional(),\n suffix: AffixValueSchema.optional(),\n })\n .strict();\n\nexport const AffixSchema = z\n .object({\n /**\n * `preserve` (default) keeps today's output: the Drizzle export name goes into the\n * identifier verbatim, so `export const users` yields `InsertusersSchema`. `pascal`\n * upper-camels it first, yielding `InsertUsersSchema`.\n */\n tableCase: z.enum(['preserve', 'pascal']).optional(),\n schema: AffixPartSchema.optional(),\n type: AffixPartSchema.optional(),\n })\n .strict();\n\n/**\n * How every relative specifier drzl invents spells its extension.\n *\n * The generated files land in the consumer's own source tree, so the consumer's\n * `moduleResolution` decides which forms resolve. `js` is the only one that resolves under\n * all of `bundler`, `node10`, `node16` and `nodenext` with no compiler flag, so it is the\n * default. See the `ImportExtension` docs in `@drzl/validation-core` for the measured grid.\n */\nexport const ImportExtensionSchema = z.enum(IMPORT_EXTENSIONS);\n\nexport const GeneratorSchema = z.object({\n kind: z.enum(['orpc', 'service', 'zod', 'valibot', 'arktype']),\n /**\n * Overrides the top-level `importExtension` for this generator alone, for a project whose\n * generated directories are compiled by different tsconfigs.\n */\n importExtension: ImportExtensionSchema.optional(),\n template: z.string().optional(),\n includeRelations: z.boolean().optional(),\n /**\n * Type `json` and `jsonb` columns from the schema rather than leaving them wide.\n *\n * `.$type<T>()` is a compile-time cast, so no runtime-derived validator can see it and\n * `drizzle-orm/zod` types every json column as its generic `Json`. A generator can reference\n * `typeof <table>.$inferSelect['<column>']` instead, which is the declared type resolved by\n * TypeScript itself, so generics, unions and imported interfaces all work.\n *\n * Off by default because it makes the generated file import your schema module, as a\n * type-only import that disappears at build time.\n */\n typedJson: z.boolean().optional(),\n naming: NamingSchema.optional(),\n outputHeader: z\n .object({\n enabled: z.boolean().default(true).optional(),\n text: z.string().optional(),\n })\n .optional(),\n format: z\n .object({\n enabled: z.boolean().default(true).optional(),\n engine: z.enum(['auto', 'prettier', 'biome']).default('auto').optional(),\n configPath: z.string().optional(),\n })\n .optional(),\n // service generator specific options\n path: z.string().optional(),\n dataAccess: z.enum(['stub', 'drizzle']).default('stub').optional(),\n dbImportPath: z.string().optional(),\n schemaImportPath: z.string().optional(),\n // zod/valibot/arktype generator specific options\n schemaSuffix: z.string().optional(),\n fileSuffix: z.string().optional(),\n /**\n * Prefixes, suffixes and table casing for generated identifiers (zod/valibot/arktype).\n * Omitting it reproduces the output of every previous release exactly.\n */\n affix: AffixSchema.optional(),\n // orpc validation sharing\n validation: z\n .object({\n useShared: z.boolean().default(false).optional(),\n library: z.enum(['zod', 'valibot', 'arktype']).default('zod').optional(),\n importPath: z.string().optional(),\n schemaSuffix: z.string().optional(),\n /**\n * How the validation generator named its exports. Usually left unset: the CLI copies\n * it from the sibling generator whose `kind` matches `library`.\n */\n affix: AffixSchema.optional(),\n })\n .optional(),\n // template options\n templateOptions: z.record(z.string(), z.any()).optional(),\n});\n\nexport const AnalyzerSchema = z.object({\n includeRelations: z.boolean().default(true),\n validateConstraints: z.boolean().default(true),\n includeHeuristicRelations: z.boolean().default(false),\n});\n\nexport const ConfigSchema = z\n .object({\n schema: z.string(),\n outDir: z.string().default('src/api'),\n /**\n * Which tables to generate for, matched against the database table name.\n *\n * There was no way to say this, and every generator loops over every table it finds, so\n * DRZL emitted unauthenticated CRUD over whatever shared the schema file. That is noise for\n * a migrations table and a genuine leak for an auth one: Better Auth puts `user`, `session`,\n * `account` and `verification` alongside your own tables, and `account` holds\n * `accessToken`, `refreshToken`, `idToken` and `password`.\n *\n * Deliberately name-based and explicit rather than detecting any particular library. Auth\n * table names are all renameable, so a built-in list would miss renamed tables and, worse,\n * silently skip an ordinary table that happened to be called `user`, which is usually the\n * application's main entity.\n *\n * `exclude` wins over `include`. Patterns support `*`, matching within a name.\n */\n include: z.array(z.string()).optional(),\n exclude: z.array(z.string()).optional(),\n /**\n * How every relative specifier drzl invents spells its extension, for every generator.\n * A generator may override it. Defaults to `js`, which is the only form that resolves\n * under every `moduleResolution` without a compiler flag.\n */\n importExtension: ImportExtensionSchema.default(DEFAULT_IMPORT_EXTENSION),\n analyzer: AnalyzerSchema.default({\n includeRelations: true,\n validateConstraints: true,\n includeHeuristicRelations: false,\n }),\n generators: z\n .array(GeneratorSchema)\n .min(1)\n .default([{ kind: 'orpc' } as any]),\n })\n // Reject an affix before anything is written, rather than emitting a file that cannot\n // compile. Only `affix` is inspected; the legacy flat `schemaSuffix` is left alone so\n // configs that parse today keep parsing.\n .superRefine((cfg, ctx) => {\n cfg.generators.forEach((g, i) => {\n const report = (base: (string | number)[], affix?: AffixOptions, schemaSuffix?: string) => {\n for (const issue of validateAffix(affix, schemaSuffix)) {\n ctx.addIssue({\n code: 'custom',\n path: ['generators', i, ...base, ...issue.path],\n message: issue.message,\n });\n }\n };\n report(['affix'], g.affix as AffixOptions | undefined, g.schemaSuffix);\n report(\n ['validation', 'affix'],\n g.validation?.affix as AffixOptions | undefined,\n g.validation?.schemaSuffix\n );\n });\n });\n\n// ✨ Separate input vs output types\nexport type DrzlConfigInput = z.input<typeof ConfigSchema>;\nexport type DrzlConfig = z.output<typeof ConfigSchema>;\n\nexport function defineConfig<T extends DrzlConfigInput>(cfg: T): T {\n return cfg;\n}\n\ntype GeneratorConfig = DrzlConfig['generators'][number];\n\nfunction sharedSchemaNames(opts: { affix?: AffixOptions; schemaSuffix?: string }): string[] {\n const resolved = resolveAffix(opts);\n return NAME_MODES.map((mode) => schemaName(mode, AFFIX_PROBE_TABLE, resolved));\n}\n\n/**\n * Fill in cross-generator defaults and refuse configs whose generators would disagree.\n *\n * An oRPC router that imports shared schemas has to spell the exact names the validation\n * generator exported. Both sides used to be configured independently, so they could silently\n * drift into a router that does not compile. When an oRPC generator uses shared validation\n * and exactly one sibling generator produces that library, its `affix` is copied across.\n *\n * Deliberately conservative about the pre-existing flat `schemaSuffix`: a disagreement there\n * is only reported, never repaired, because repairing it would change the bytes an existing\n * config emits.\n *\n * `importExtension` is pushed down here too. A consumer compiles the whole generated tree\n * with one tsconfig, so the setting that has to hold is the same for every generator, and\n * every call site downstream can then read it off the generator without knowing about the\n * top-level default.\n */\nexport function resolveConfig(cfg: DrzlConfig): { config: DrzlConfig; warnings: string[] } {\n const warnings: string[] = [];\n const generators: GeneratorConfig[] = cfg.generators.map((g) => ({\n ...g,\n importExtension: g.importExtension ?? cfg.importExtension,\n }));\n\n for (const g of generators) {\n if (g.kind !== 'orpc') continue;\n const v = g.validation;\n if (!v?.useShared) continue;\n\n const library = v.library ?? 'zod';\n const siblings = generators.filter((s) => s.kind === library);\n // Zero siblings means the user points at a barrel drzl does not generate; more than one\n // means there is no single source of truth. Either way, leave the config alone.\n if (siblings.length !== 1) continue;\n const sibling = siblings[0];\n\n const theirs = sharedSchemaNames({\n affix: sibling.affix as AffixOptions | undefined,\n schemaSuffix: sibling.schemaSuffix,\n });\n\n if (!v.affix) {\n if (sibling.affix) {\n // Bake the sibling's fully resolved naming in, so its own schemaSuffix fallback\n // travels with it and cannot be re-interpreted on the oRPC side.\n g.validation = {\n ...v,\n affix: resolveAffix({\n affix: sibling.affix as AffixOptions,\n schemaSuffix: sibling.schemaSuffix,\n }),\n };\n continue;\n }\n const mine = sharedSchemaNames({ schemaSuffix: v.schemaSuffix });\n if (mine.join(',') !== theirs.join(',')) {\n warnings.push(\n `drzl config: the \"orpc\" generator's validation.schemaSuffix ` +\n `(${JSON.stringify(v.schemaSuffix ?? 'Schema')}) does not match the \"${library}\" ` +\n `generator's schemaSuffix (${JSON.stringify(sibling.schemaSuffix ?? 'Schema')}). ` +\n `The router will import ${mine.join(', ')} but the \"${library}\" generator exports ` +\n `${theirs.join(', ')}, so the generated router will not compile. Set both to the ` +\n `same value, or move to \"affix\", which is inherited automatically.`\n );\n }\n continue;\n }\n\n const mine = sharedSchemaNames({\n affix: v.affix as AffixOptions,\n schemaSuffix: v.schemaSuffix,\n });\n if (mine.join(',') !== theirs.join(',')) {\n throw new Error(\n `drzl config: the \"orpc\" generator imports shared ${library} schemas, but its ` +\n `validation.affix disagrees with the \"${library}\" generator's own naming. The router ` +\n `would import ${mine.join(', ')} while the \"${library}\" generator exports ` +\n `${theirs.join(', ')}. Make them match, or drop validation.affix and let it be ` +\n `inherited from the \"${library}\" generator.`\n );\n }\n }\n\n return { config: { ...cfg, generators }, warnings };\n}\n\n/**\n * Parse, then resolve cross-generator defaults. Both `generate` and `watch` go through\n * loadConfig, so putting the resolution here is what keeps the two duplicated generator\n * dispatch blocks in cli.ts from needing the logic twice.\n */\nfunction finalize(raw: unknown): DrzlConfig {\n const { config, warnings } = resolveConfig(ConfigSchema.parse(raw));\n for (const w of warnings) console.warn(w);\n return config;\n}\n\nexport async function loadConfig(customPath?: string): Promise<DrzlConfig | null> {\n const fsp = await import('node:fs/promises');\n\n const candidates = customPath\n ? [customPath]\n : [\n 'drzl.config.ts',\n 'drzl.config.mjs',\n 'drzl.config.js',\n 'drzl.config.cjs',\n 'drzl.config.json',\n ];\n\n for (const c of candidates) {\n const p = path.resolve(process.cwd(), c);\n try {\n await fsp.access(p);\n } catch {\n continue;\n }\n\n const ext = path.extname(p).toLowerCase();\n\n // JSON: read directly\n if (ext === '.json') {\n const raw = JSON.parse(await fsp.readFile(p, 'utf8'));\n return finalize(raw);\n }\n\n // Everything else (TS/JS/MJS/CJS) -> Jiti with cache-busting\n const { createJiti } = await import('jiti');\n const stat = await fsp.stat(p);\n\n // Passing __filename is safe in CJS; fallback to cwd if not defined.\n const base =\n typeof __filename !== 'undefined' ? __filename : path.join(process.cwd(), 'index.js');\n\n const jiti = createJiti(base, {\n moduleCache: false, // re-evaluate each time\n fsCache: true, // keep transform cache\n cacheVersion: String(stat.mtimeMs), // bump on edit\n interopDefault: true,\n tryNative: false, // <-- prevent native import of .ts\n // debug: true,\n }) as any;\n\n const mod = await jiti.import(p);\n const raw = mod?.default ?? mod;\n return finalize(raw);\n }\n\n return null;\n}\n\n/** Absolute output dirs for all generators (to ignore in watcher). */\nexport function computeGeneratorOutputDirs(cfg: DrzlConfig, cwd = process.cwd()): string[] {\n const abs = (p: string) => path.resolve(cwd, p);\n const dirs = new Set<string>();\n dirs.add(abs(cfg.outDir)); // orpc\n for (const g of cfg.generators) {\n if (g.kind === 'service') dirs.add(abs(g.path ?? 'src/services'));\n if (g.kind === 'zod') dirs.add(abs(g.path ?? 'src/validators/zod'));\n if (g.kind === 'valibot') dirs.add(abs(g.path ?? 'src/validators/valibot'));\n if (g.kind === 'arktype') dirs.add(abs(g.path ?? 'src/validators/arktype'));\n }\n return [...dirs];\n}\n\n/** Resolve custom template directories (local path or installed package). */\nexport function resolveTemplateDirsSync(cfg: DrzlConfig, cwd = process.cwd()): string[] {\n const results: string[] = [];\n const req = createRequire(\n typeof __filename !== 'undefined' ? __filename : path.join(process.cwd(), 'index.js')\n );\n\n for (const g of cfg.generators) {\n const t = g.template;\n if (!t || t === 'standard' || t === 'minimal') continue;\n\n // Try package resolution relative to cwd\n let pkgDir: string | null = null;\n try {\n const pkg = req.resolve(`${t}/package.json`, { paths: [cwd] as any });\n pkgDir = path.dirname(pkg);\n } catch {}\n\n if (pkgDir) {\n results.push(pkgDir);\n continue;\n }\n\n // Local path-like template\n if (/[./\\\\]/.test(t)) {\n const abs = path.resolve(cwd, t);\n if (fs.existsSync(abs)) results.push(abs);\n }\n }\n\n return Array.from(new Set(results));\n}\n\n/** Build watch targets (exclude output dirs; watcher will ignore those). */\n/**\n * Narrow an analysis's tables to the ones the config asked for.\n *\n * Matching is on the database table name, anchored, with `*` as the only metacharacter. Anchored\n * matters: `user` must not also drop `users`, and a substring match would. `exclude` is applied\n * after `include`, so the safer direction wins when both name the same table.\n */\nexport function filterTables<T extends { name: string }>(\n tables: T[],\n opts: { include?: string[]; exclude?: string[] }\n): T[] {\n const toRegExp = (pattern: string) =>\n new RegExp(\n '^' +\n pattern\n .split('*')\n .map((part) => part.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n .join('.*') +\n '$'\n );\n\n const matches = (patterns: string[], name: string) =>\n patterns.some((p) => toRegExp(p).test(name));\n\n let out = tables;\n if (opts.include?.length) out = out.filter((t) => matches(opts.include!, t.name));\n if (opts.exclude?.length) out = out.filter((t) => !matches(opts.exclude!, t.name));\n return out;\n}\n\nexport function computeWatchTargets(cfg: DrzlConfig, cwd = process.cwd()): string[] {\n const abs = (p: string) => path.resolve(cwd, p);\n const schemaAbs = abs(cfg.schema);\n // The schema's directory, not a glob under it. Chokidar removed glob support in v4 and treats\n // `<dir>/**/*.{ts,tsx,js}` as a literal path, so it watched a directory named `**` that does\n // not exist: no event ever fired and `drzl watch` did its initial build and then sat inert.\n // A directory is watched recursively by chokidar itself, and the extension filtering that the\n // glob was doing now happens on the event instead.\n const targets = new Set<string>([\n path.dirname(schemaAbs),\n abs('drzl.config.ts'),\n abs('drzl.config.js'),\n abs('drzl.config.mjs'),\n abs('drzl.config.cjs'),\n ]);\n for (const t of resolveTemplateDirsSync(cfg, cwd)) targets.add(t);\n return [...targets];\n}\n"],"mappings":"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","names":["mine","raw"]}
package/dist/cli.cjs CHANGED
@@ -30,7 +30,7 @@ var import_chalk2 = __toESM(require("chalk"), 1);
30
30
  var import_chokidar = __toESM(require("chokidar"), 1);
31
31
  var import_cli_progress = __toESM(require("cli-progress"), 1);
32
32
  var import_commander = require("commander");
33
- var path3 = __toESM(require("path"), 1);
33
+ var path4 = __toESM(require("path"), 1);
34
34
  var import_ora = __toESM(require("ora"), 1);
35
35
 
36
36
  // src/config.ts
@@ -80,6 +80,18 @@ var GeneratorSchema = import_zod.z.object({
80
80
  importExtension: ImportExtensionSchema.optional(),
81
81
  template: import_zod.z.string().optional(),
82
82
  includeRelations: import_zod.z.boolean().optional(),
83
+ /**
84
+ * Type `json` and `jsonb` columns from the schema rather than leaving them wide.
85
+ *
86
+ * `.$type<T>()` is a compile-time cast, so no runtime-derived validator can see it and
87
+ * `drizzle-orm/zod` types every json column as its generic `Json`. A generator can reference
88
+ * `typeof <table>.$inferSelect['<column>']` instead, which is the declared type resolved by
89
+ * TypeScript itself, so generics, unions and imported interfaces all work.
90
+ *
91
+ * Off by default because it makes the generated file import your schema module, as a
92
+ * type-only import that disappears at build time.
93
+ */
94
+ typedJson: import_zod.z.boolean().optional(),
83
95
  naming: NamingSchema.optional(),
84
96
  outputHeader: import_zod.z.object({
85
97
  enabled: import_zod.z.boolean().default(true).optional(),
@@ -126,6 +138,24 @@ var AnalyzerSchema = import_zod.z.object({
126
138
  var ConfigSchema = import_zod.z.object({
127
139
  schema: import_zod.z.string(),
128
140
  outDir: import_zod.z.string().default("src/api"),
141
+ /**
142
+ * Which tables to generate for, matched against the database table name.
143
+ *
144
+ * There was no way to say this, and every generator loops over every table it finds, so
145
+ * DRZL emitted unauthenticated CRUD over whatever shared the schema file. That is noise for
146
+ * a migrations table and a genuine leak for an auth one: Better Auth puts `user`, `session`,
147
+ * `account` and `verification` alongside your own tables, and `account` holds
148
+ * `accessToken`, `refreshToken`, `idToken` and `password`.
149
+ *
150
+ * Deliberately name-based and explicit rather than detecting any particular library. Auth
151
+ * table names are all renameable, so a built-in list would miss renamed tables and, worse,
152
+ * silently skip an ordinary table that happened to be called `user`, which is usually the
153
+ * application's main entity.
154
+ *
155
+ * `exclude` wins over `include`. Patterns support `*`, matching within a name.
156
+ */
157
+ include: import_zod.z.array(import_zod.z.string()).optional(),
158
+ exclude: import_zod.z.array(import_zod.z.string()).optional(),
129
159
  /**
130
160
  * How every relative specifier drzl invents spells its extension, for every generator.
131
161
  * A generator may override it. Defaults to `js`, which is the only form that resolves
@@ -294,6 +324,16 @@ function resolveTemplateDirsSync(cfg, cwd = process.cwd()) {
294
324
  }
295
325
  return Array.from(new Set(results));
296
326
  }
327
+ function filterTables(tables, opts) {
328
+ const toRegExp = (pattern) => new RegExp(
329
+ "^" + pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
330
+ );
331
+ const matches = (patterns, name) => patterns.some((p) => toRegExp(p).test(name));
332
+ let out = tables;
333
+ if (opts.include?.length) out = out.filter((t) => matches(opts.include, t.name));
334
+ if (opts.exclude?.length) out = out.filter((t) => !matches(opts.exclude, t.name));
335
+ return out;
336
+ }
297
337
  function computeWatchTargets(cfg, cwd = process.cwd()) {
298
338
  const abs = (p) => path.resolve(cwd, p);
299
339
  const schemaAbs = abs(cfg.schema);
@@ -308,12 +348,63 @@ function computeWatchTargets(cfg, cwd = process.cwd()) {
308
348
  return [...targets];
309
349
  }
310
350
 
311
- // src/sponsor.ts
312
- var import_chalk = __toESM(require("chalk"), 1);
351
+ // src/drift.ts
313
352
  var import_node_fs = require("fs");
314
353
  var import_node_path = __toESM(require("path"), 1);
315
- var CACHE_DIR = import_node_path.default.join(process.cwd(), "node_modules", ".cache", "@drzl");
316
- var CACHE_FILE = import_node_path.default.join(CACHE_DIR, "sponsor-message.json");
354
+ async function snapshotDir(dir) {
355
+ const out = /* @__PURE__ */ new Map();
356
+ async function walk(current) {
357
+ let entries;
358
+ try {
359
+ entries = await import_node_fs.promises.readdir(current, { withFileTypes: true });
360
+ } catch {
361
+ return;
362
+ }
363
+ for (const e of entries) {
364
+ const full = import_node_path.default.join(current, e.name);
365
+ if (e.isDirectory()) await walk(full);
366
+ else out.set(import_node_path.default.relative(dir, full), await import_node_fs.promises.readFile(full, "utf8"));
367
+ }
368
+ }
369
+ await walk(dir);
370
+ return out;
371
+ }
372
+ async function snapshotAll(dirs) {
373
+ const all = /* @__PURE__ */ new Map();
374
+ for (const dir of dirs) {
375
+ for (const [rel, content] of await snapshotDir(dir)) {
376
+ all.set(import_node_path.default.join(dir, rel), content);
377
+ }
378
+ }
379
+ return all;
380
+ }
381
+ function diffSnapshots(before, after) {
382
+ const out = [];
383
+ for (const [file, content] of after) {
384
+ if (!before.has(file)) out.push({ file, status: "added" });
385
+ else if (before.get(file) !== content) out.push({ file, status: "changed" });
386
+ }
387
+ for (const file of before.keys()) {
388
+ if (!after.has(file)) out.push({ file, status: "removed" });
389
+ }
390
+ return out.sort((a, b) => a.file.localeCompare(b.file));
391
+ }
392
+ async function restoreSnapshot(before, after) {
393
+ for (const [file, content] of before) {
394
+ await import_node_fs.promises.mkdir(import_node_path.default.dirname(file), { recursive: true });
395
+ await import_node_fs.promises.writeFile(file, content, "utf8");
396
+ }
397
+ for (const file of after.keys()) {
398
+ if (!before.has(file)) await import_node_fs.promises.rm(file, { force: true });
399
+ }
400
+ }
401
+
402
+ // src/sponsor.ts
403
+ var import_chalk = __toESM(require("chalk"), 1);
404
+ var import_node_fs2 = require("fs");
405
+ var import_node_path2 = __toESM(require("path"), 1);
406
+ var CACHE_DIR = import_node_path2.default.join(process.cwd(), "node_modules", ".cache", "@drzl");
407
+ var CACHE_FILE = import_node_path2.default.join(CACHE_DIR, "sponsor-message.json");
317
408
  var DEFAULT_INTERVAL_MS = 1e3 * 60 * 15;
318
409
  var shownThisProcess = false;
319
410
  var tips = [
@@ -334,7 +425,7 @@ function maybeShowSponsorMessage({
334
425
  const hideRequested = hideViaEnv === "1" || hideViaEnv === "true";
335
426
  if (hideRequested || process.env.CI && !force || shownThisProcess && !force) return;
336
427
  try {
337
- (0, import_node_fs.mkdirSync)(CACHE_DIR, { recursive: true });
428
+ (0, import_node_fs2.mkdirSync)(CACHE_DIR, { recursive: true });
338
429
  const payload = readCache();
339
430
  payload.runs += 1;
340
431
  const now = Date.now();
@@ -361,11 +452,11 @@ ${green("Pro tip:")} ${tip}
361
452
  }
362
453
  }
363
454
  function readCache() {
364
- if (!(0, import_node_fs.existsSync)(CACHE_FILE)) {
455
+ if (!(0, import_node_fs2.existsSync)(CACHE_FILE)) {
365
456
  return { runs: 0 };
366
457
  }
367
458
  try {
368
- const data = JSON.parse((0, import_node_fs.readFileSync)(CACHE_FILE, "utf8"));
459
+ const data = JSON.parse((0, import_node_fs2.readFileSync)(CACHE_FILE, "utf8"));
369
460
  if (typeof data.runs !== "number") return { runs: 0 };
370
461
  return data;
371
462
  } catch {
@@ -373,7 +464,7 @@ function readCache() {
373
464
  }
374
465
  }
375
466
  function writeCache(payload) {
376
- (0, import_node_fs.writeFileSync)(CACHE_FILE, JSON.stringify(payload, null, 2), "utf8");
467
+ (0, import_node_fs2.writeFileSync)(CACHE_FILE, JSON.stringify(payload, null, 2), "utf8");
377
468
  }
378
469
 
379
470
  // src/cli.ts
@@ -400,8 +491,8 @@ program.command("analyze").argument("<schema>", "path to drizzle schema (TS)").o
400
491
  if (opts.json) {
401
492
  console.log(json);
402
493
  } else if (opts.out) {
403
- const fs2 = await import("fs/promises");
404
- await fs2.writeFile(opts.out, json, "utf8");
494
+ const fs3 = await import("fs/promises");
495
+ await fs3.writeFile(opts.out, json, "utf8");
405
496
  spinner?.succeed(import_chalk2.default.green(`Analysis written to ${opts.out} in ${ms}ms`));
406
497
  } else {
407
498
  spinner?.succeed(import_chalk2.default.green(`Analyzed in ${ms}ms`));
@@ -421,7 +512,10 @@ program.command("analyze").argument("<schema>", "path to drizzle schema (TS)").o
421
512
  process.exit(1);
422
513
  }
423
514
  });
424
- program.command("generate").description("Run configured generators (drzl.config.*)").option("-c, --config <path>", "path to drzl.config").action(async (opts) => {
515
+ program.command("generate").description("Run configured generators (drzl.config.*)").option("-c, --config <path>", "path to drzl.config").option(
516
+ "--check",
517
+ "regenerate and fail if the result differs from what is on disk, without changing it"
518
+ ).action(async (opts) => {
425
519
  try {
426
520
  const cfg = await loadConfig(opts.config);
427
521
  if (!cfg) {
@@ -439,7 +533,10 @@ program.command("generate").description("Run configured generators (drzl.config.
439
533
  validateConstraints: cfg.analyzer.validateConstraints,
440
534
  includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
441
535
  });
536
+ analysis.tables = filterTables(analysis.tables, cfg);
442
537
  spinner.succeed(`Analysis complete in ${Date.now() - t0}ms`);
538
+ const driftDirs = computeGeneratorOutputDirs(cfg);
539
+ const driftBefore = opts.check ? await snapshotAll(driftDirs) : null;
443
540
  const progress = new import_cli_progress.default.SingleBar(
444
541
  { hideCursor: true },
445
542
  import_cli_progress.default.Presets.shades_classic
@@ -504,7 +601,11 @@ program.command("generate").description("Run configured generators (drzl.config.
504
601
  schemaSuffix: g.schemaSuffix,
505
602
  fileSuffix: g.fileSuffix,
506
603
  importExtension: g.importExtension,
507
- affix: g.affix
604
+ affix: g.affix,
605
+ // Needed by typedJson, which imports the schema back to reference the type
606
+ // Drizzle inferred for a json column.
607
+ schemaPath: cfg.schema,
608
+ typedJson: g.typedJson
508
609
  });
509
610
  progress.stop();
510
611
  (0, import_ora.default)().succeed(import_chalk2.default.green(`Generated (zod): ${files.length} files`));
@@ -572,6 +673,27 @@ program.command("generate").description("Run configured generators (drzl.config.
572
673
  }
573
674
  }
574
675
  }
676
+ if (driftBefore) {
677
+ const after = await snapshotAll(driftDirs);
678
+ const drift = diffSnapshots(driftBefore, after);
679
+ await restoreSnapshot(driftBefore, after);
680
+ if (drift.length) {
681
+ console.error(
682
+ import_chalk2.default.red(`
683
+ Generated output is out of date (${drift.length} file(s)):`)
684
+ );
685
+ for (const d of drift) {
686
+ const mark = d.status === "added" ? "+" : d.status === "removed" ? "-" : "~";
687
+ console.error(` ${mark} ${import_chalk2.default.yellow(d.status.padEnd(8))} ${path4.relative(process.cwd(), d.file)}`);
688
+ }
689
+ console.error(
690
+ import_chalk2.default.dim("\nRun `drzl generate` and commit the result. Nothing was written by this check.")
691
+ );
692
+ process.exit(1);
693
+ }
694
+ console.log(import_chalk2.default.green("Generated output is up to date."));
695
+ return;
696
+ }
575
697
  if (cfg.generators.length) {
576
698
  maybeShowSponsorMessage({ reason: "generate" });
577
699
  }
@@ -611,10 +733,10 @@ program.command("watch").description("Watch schema and regenerate on changes").o
611
733
  process.exit(2);
612
734
  return;
613
735
  }
614
- const abs = (p) => path3.resolve(process.cwd(), p);
736
+ const abs = (p) => path4.resolve(process.cwd(), p);
615
737
  const isInside = (child, parent) => {
616
- const rel = path3.relative(parent, child);
617
- return !!rel && !rel.startsWith("..") && !path3.isAbsolute(rel);
738
+ const rel = path4.relative(parent, child);
739
+ return !!rel && !rel.startsWith("..") && !path4.isAbsolute(rel);
618
740
  };
619
741
  const ignoredOutDirs = new Set(computeGeneratorOutputDirs(cfg).map(abs));
620
742
  const currentTargets = new Set(computeWatchTargets(cfg).map(abs));
@@ -639,7 +761,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
639
761
  if (full === dir || isInside(full, dir)) return true;
640
762
  }
641
763
  if (stats?.isDirectory()) return false;
642
- const ext = path3.extname(full);
764
+ const ext = path4.extname(full);
643
765
  if (!ext) return false;
644
766
  return !WATCHED_EXTENSIONS.has(ext);
645
767
  };
@@ -687,6 +809,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
687
809
  validateConstraints: cfg.analyzer.validateConstraints,
688
810
  includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
689
811
  });
812
+ analysis.tables = filterTables(analysis.tables, cfg);
690
813
  if (opts.pipeline === "analyze") {
691
814
  if (opts.json) {
692
815
  console.log(
@@ -872,7 +995,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
872
995
  } else {
873
996
  console.log(
874
997
  import_chalk2.default.gray(
875
- "Watching:\n " + Array.from(currentTargets).map((p) => path3.relative(process.cwd(), p)).join("\n ")
998
+ "Watching:\n " + Array.from(currentTargets).map((p) => path4.relative(process.cwd(), p)).join("\n ")
876
999
  )
877
1000
  );
878
1001
  }
@@ -880,9 +1003,9 @@ program.command("watch").description("Watch schema and regenerate on changes").o
880
1003
  await run();
881
1004
  });
882
1005
  program.command("init").description("Scaffold a drzl.config.ts").option("-y, --yes", "accept defaults").action(async (_opts) => {
883
- const fs2 = await import("fs/promises");
884
- const path4 = await import("path");
885
- const target = path4.resolve(process.cwd(), "drzl.config.ts");
1006
+ const fs3 = await import("fs/promises");
1007
+ const path5 = await import("path");
1008
+ const target = path5.resolve(process.cwd(), "drzl.config.ts");
886
1009
  const template = `export default {
887
1010
  schema: 'src/db/schema.ts',
888
1011
  outDir: 'src/api',
@@ -893,7 +1016,7 @@ program.command("init").description("Scaffold a drzl.config.ts").option("-y, --y
893
1016
  } as const
894
1017
  `;
895
1018
  try {
896
- await fs2.writeFile(target, template, { flag: "wx" });
1019
+ await fs3.writeFile(target, template, { flag: "wx" });
897
1020
  console.log(import_chalk2.default.green(`Created ${target}`));
898
1021
  } catch (e) {
899
1022
  console.error(import_chalk2.default.red("Init failed:"), e?.message ?? e);