@drzl/cli 4.38.0 → 4.40.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.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  import { qualifiedTableName as qualifiedTableName2, SchemaAnalyzer as SchemaAnalyzer2 } from "@drzl/analyzer";
44
44
  import chokidar from "chokidar";
45
45
  import { Command } from "commander";
46
- import * as path7 from "path";
46
+ import * as path8 from "path";
47
47
 
48
48
  // src/output.ts
49
49
  import { Chalk } from "chalk";
@@ -1254,11 +1254,11 @@ function authTableWarnings(analysis) {
1254
1254
  import { Chalk as Chalk2 } from "chalk";
1255
1255
  var PLAIN = new Chalk2({ level: 0 });
1256
1256
  var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
1257
- function splitPath(path8) {
1258
- if (!path8) return {};
1259
- const dot = path8.lastIndexOf(".");
1260
- if (dot <= 0) return { table: path8 };
1261
- return { table: path8.slice(0, dot), column: path8.slice(dot + 1) };
1257
+ function splitPath(path9) {
1258
+ if (!path9) return {};
1259
+ const dot = path9.lastIndexOf(".");
1260
+ if (dot <= 0) return { table: path9 };
1261
+ return { table: path9.slice(0, dot), column: path9.slice(dot + 1) };
1262
1262
  }
1263
1263
  function namedColumns(parsed) {
1264
1264
  const out = [];
@@ -1810,11 +1810,11 @@ function issueTouches(issue, table) {
1810
1810
  return dot > 0 && own.includes(issue.path.slice(0, dot));
1811
1811
  }
1812
1812
  function issueColumn(issue, table) {
1813
- const path8 = issue.path ?? "";
1813
+ const path9 = issue.path ?? "";
1814
1814
  const names = namesOf(table);
1815
1815
  for (const prefix of [names.qualified, names.tsName, names.name, table.name]) {
1816
- if (path8.startsWith(`${prefix}.`)) {
1817
- const rest = path8.slice(prefix.length + 1);
1816
+ if (path9.startsWith(`${prefix}.`)) {
1817
+ const rest = path9.slice(prefix.length + 1);
1818
1818
  if (table.columns.some((c) => c.name === rest)) return rest;
1819
1819
  }
1820
1820
  }
@@ -2973,6 +2973,107 @@ function displayPath(file, cwd = process.cwd()) {
2973
2973
  return rel && !rel.startsWith("..") ? rel : file;
2974
2974
  }
2975
2975
 
2976
+ // src/manifest.ts
2977
+ import path4 from "path";
2978
+ import { promises as fs4 } from "fs";
2979
+ var MANIFEST_VERSION = 1;
2980
+ var MANIFEST_PATH = path4.join(".drzl", "manifest.json");
2981
+ function toPosix(p) {
2982
+ return p.split(path4.sep).join("/");
2983
+ }
2984
+ function manifestEntries(files, root) {
2985
+ const rel = files.map((f) => toPosix(path4.relative(root, f)));
2986
+ return [...new Set(rel)].sort();
2987
+ }
2988
+ async function readManifest(root) {
2989
+ try {
2990
+ const raw = await fs4.readFile(path4.join(root, MANIFEST_PATH), "utf8");
2991
+ const parsed = JSON.parse(raw);
2992
+ if (!parsed || typeof parsed !== "object" || parsed.version !== MANIFEST_VERSION || !Array.isArray(parsed.files) || !parsed.files.every((f) => typeof f === "string")) {
2993
+ return void 0;
2994
+ }
2995
+ return { version: MANIFEST_VERSION, files: parsed.files };
2996
+ } catch {
2997
+ return void 0;
2998
+ }
2999
+ }
3000
+ async function writeManifest(root, files) {
3001
+ const manifest = { version: MANIFEST_VERSION, files: manifestEntries(files, root) };
3002
+ const target = path4.join(root, MANIFEST_PATH);
3003
+ await fs4.mkdir(path4.dirname(target), { recursive: true });
3004
+ await fs4.writeFile(target, `${JSON.stringify(manifest, null, 2)}
3005
+ `, "utf8");
3006
+ }
3007
+ async function staleFiles(previous, writtenNow, root) {
3008
+ if (!previous) return [];
3009
+ const current = new Set(manifestEntries(writtenNow, root));
3010
+ const gone = previous.files.filter((f) => !current.has(f));
3011
+ const out = [];
3012
+ for (const file of gone) {
3013
+ let present = true;
3014
+ try {
3015
+ await fs4.access(path4.join(root, file));
3016
+ } catch {
3017
+ present = false;
3018
+ }
3019
+ out.push({ file, present });
3020
+ }
3021
+ return out;
3022
+ }
3023
+ async function pruneStale(root, stale) {
3024
+ const deleted = [];
3025
+ for (const entry of stale) {
3026
+ if (!entry.present) continue;
3027
+ const target = path4.resolve(root, entry.file);
3028
+ const inside = target === root || target.startsWith(root + path4.sep);
3029
+ if (!inside) continue;
3030
+ try {
3031
+ await fs4.rm(target);
3032
+ deleted.push(entry.file);
3033
+ } catch {
3034
+ }
3035
+ }
3036
+ return deleted;
3037
+ }
3038
+ function staleWarning(stale) {
3039
+ const present = stale.filter((s) => s.present);
3040
+ if (!present.length) return void 0;
3041
+ const list = present.map((s) => s.file).join(", ");
3042
+ return `drzl generate: ${present.length} file(s) written by a previous run were not written by this one, and are still on disk: ${list}. That usually means a table was renamed or removed. Delete them with \`drzl generate --prune\`, which removes only files a previous run recorded writing.`;
3043
+ }
3044
+ function nextManifestFiles(writtenNow, stale, root) {
3045
+ const kept = stale.filter((s) => s.present).map((s) => path4.resolve(root, s.file));
3046
+ return manifestEntries([...writtenNow, ...kept], root);
3047
+ }
3048
+
3049
+ // src/rebuild-fingerprint.ts
3050
+ import { createHash } from "crypto";
3051
+ function analysisFingerprint(analysis) {
3052
+ const material = {
3053
+ dialect: analysis.dialect,
3054
+ tables: analysis.tables,
3055
+ enums: analysis.enums,
3056
+ relations: analysis.relations
3057
+ };
3058
+ return createHash("sha256").update(stableStringify(material)).digest("hex");
3059
+ }
3060
+ function configFingerprint(generators) {
3061
+ return createHash("sha256").update(stableStringify(generators)).digest("hex");
3062
+ }
3063
+ function stableStringify(value) {
3064
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
3065
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
3066
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
3067
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
3068
+ }
3069
+ function rebuildSignature(analysis, generators) {
3070
+ return { analysis: analysisFingerprint(analysis), config: configFingerprint(generators) };
3071
+ }
3072
+ function sameAsLast(previous, next) {
3073
+ if (!previous) return false;
3074
+ return previous.analysis === next.analysis && previous.config === next.config;
3075
+ }
3076
+
2976
3077
  // src/unified-diff.ts
2977
3078
  var DEFAULT_DIFF_LIMITS = {
2978
3079
  maxLines: 4e3,
@@ -3215,8 +3316,8 @@ function createRebuildScheduler(options) {
3215
3316
 
3216
3317
  // src/init.ts
3217
3318
  import { SchemaAnalyzer } from "@drzl/analyzer";
3218
- import * as fs4 from "fs";
3219
- import * as path4 from "path";
3319
+ import * as fs5 from "fs";
3320
+ import * as path5 from "path";
3220
3321
  var INIT_GENERATOR_CHOICES = [
3221
3322
  { kind: "zod", packageName: "@drzl/generator-zod", label: "Zod validators" },
3222
3323
  { kind: "valibot", packageName: "@drzl/generator-valibot", label: "Valibot validators" },
@@ -3287,7 +3388,7 @@ async function detectSchema(cwd) {
3287
3388
  kitFiles = null;
3288
3389
  }
3289
3390
  if (kitFiles && kitPath) {
3290
- const rel = path4.relative(cwd, kitPath) || path4.basename(kitPath);
3391
+ const rel = path5.relative(cwd, kitPath) || path5.basename(kitPath);
3291
3392
  const report = await classifySchemaCandidate(kitFiles);
3292
3393
  if (report.verdict === "confirmed" || report.verdict === "unverified") {
3293
3394
  notes.push(
@@ -3303,10 +3404,10 @@ async function detectSchema(cwd) {
3303
3404
  }
3304
3405
  notes.push(`${rel} names schema files that declare no Drizzle tables; looking elsewhere.`);
3305
3406
  }
3306
- const present = schemaCandidates().filter((c) => fs4.existsSync(path4.resolve(cwd, c)));
3407
+ const present = schemaCandidates().filter((c) => fs5.existsSync(path5.resolve(cwd, c)));
3307
3408
  const unverified = [];
3308
3409
  for (const file of present) {
3309
- const report = await classifySchemaCandidate(path4.resolve(cwd, file));
3410
+ const report = await classifySchemaCandidate(path5.resolve(cwd, file));
3310
3411
  if (report.verdict === "confirmed") {
3311
3412
  notes.push(
3312
3413
  `Schema found at ${file} (${report.tables} table${report.tables === 1 ? "" : "s"})`
@@ -3408,7 +3509,7 @@ async function promptForPlan(args) {
3408
3509
  if (answer === null) endedEarly = true;
3409
3510
  else if (answer.trim()) {
3410
3511
  const typed = answer.trim();
3411
- const report = await classifySchemaCandidate(path4.resolve(cwd, typed));
3512
+ const report = await classifySchemaCandidate(path5.resolve(cwd, typed));
3412
3513
  if (report.verdict === "confirmed") {
3413
3514
  write(` ${typed}: ${report.tables} table${report.tables === 1 ? "" : "s"}`);
3414
3515
  } else if (report.verdict === "unverified") {
@@ -3470,8 +3571,8 @@ function parseGeneratorsFlag(value) {
3470
3571
  return parts;
3471
3572
  }
3472
3573
  async function runInit(args) {
3473
- const target = path4.resolve(args.cwd, "drzl.config.ts");
3474
- const existing = CONFIG_FILE_NAMES.find((name) => fs4.existsSync(path4.resolve(args.cwd, name)));
3574
+ const target = path5.resolve(args.cwd, "drzl.config.ts");
3575
+ const existing = CONFIG_FILE_NAMES.find((name) => fs5.existsSync(path5.resolve(args.cwd, name)));
3475
3576
  if (existing) {
3476
3577
  args.error(
3477
3578
  `drzl init: ${existing} already exists, so nothing was written. Delete it, or edit it by hand; init never overwrites a config, and will not write one that shadows it either.`
@@ -3510,8 +3611,8 @@ async function runInit(args) {
3510
3611
  generators: fromFlag ?? [DEFAULT_GENERATOR_KIND]
3511
3612
  };
3512
3613
  if (args.schemaFlag) {
3513
- const full = path4.resolve(args.cwd, args.schemaFlag);
3514
- if (!fs4.existsSync(full)) {
3614
+ const full = path5.resolve(args.cwd, args.schemaFlag);
3615
+ if (!fs5.existsSync(full)) {
3515
3616
  args.log(`--schema ${args.schemaFlag} is not there yet. Writing it anyway.`);
3516
3617
  } else if ((await classifySchemaCandidate(full)).verdict === "rejected") {
3517
3618
  args.log(`--schema ${args.schemaFlag} declares no Drizzle tables. Writing it anyway.`);
@@ -3519,7 +3620,7 @@ async function runInit(args) {
3519
3620
  }
3520
3621
  }
3521
3622
  try {
3522
- fs4.writeFileSync(target, renderInitConfig(plan), { flag: "wx" });
3623
+ fs5.writeFileSync(target, renderInitConfig(plan), { flag: "wx" });
3523
3624
  } catch (e) {
3524
3625
  if (e?.code === "EEXIST") {
3525
3626
  args.error(
@@ -3543,9 +3644,9 @@ async function runInit(args) {
3543
3644
 
3544
3645
  // src/sponsor.ts
3545
3646
  import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
3546
- import path5 from "path";
3547
- var CACHE_DIR = path5.join(process.cwd(), "node_modules", ".cache", "@drzl");
3548
- var CACHE_FILE = path5.join(CACHE_DIR, "sponsor-message.json");
3647
+ import path6 from "path";
3648
+ var CACHE_DIR = path6.join(process.cwd(), "node_modules", ".cache", "@drzl");
3649
+ var CACHE_FILE = path6.join(CACHE_DIR, "sponsor-message.json");
3549
3650
  var DEFAULT_INTERVAL_MS = 1e3 * 60 * 15;
3550
3651
  var shownThisProcess = false;
3551
3652
  var tips = [
@@ -3614,11 +3715,11 @@ function writeCache(payload) {
3614
3715
 
3615
3716
  // src/version.ts
3616
3717
  import { readFileSync as readFileSync2 } from "fs";
3617
- import * as path6 from "path";
3718
+ import * as path7 from "path";
3618
3719
  import { fileURLToPath } from "url";
3619
3720
  var PACKAGE_NAME = "@drzl/cli";
3620
3721
  function moduleDir() {
3621
- return path6.dirname(fileURLToPath(import.meta.url));
3722
+ return path7.dirname(fileURLToPath(import.meta.url));
3622
3723
  }
3623
3724
  function readVersionFrom(manifestPath) {
3624
3725
  let raw;
@@ -3641,7 +3742,7 @@ function readVersionFrom(manifestPath) {
3641
3742
  return manifest.version;
3642
3743
  }
3643
3744
  function readCliVersion() {
3644
- return readVersionFrom(path6.join(moduleDir(), "..", "package.json"));
3745
+ return readVersionFrom(path7.join(moduleDir(), "..", "package.json"));
3645
3746
  }
3646
3747
  var CLI_VERSION = readCliVersion();
3647
3748
 
@@ -3730,8 +3831,8 @@ withOutputFlags(
3730
3831
  const errors = res.issues.some((i) => i.level === "error");
3731
3832
  const code = unreadable ? EXIT_FAILED : errors ? EXIT_FINDINGS : EXIT_OK;
3732
3833
  if (opts.out && !opts.json) {
3733
- const fs5 = await import("fs/promises");
3734
- await fs5.writeFile(opts.out, JSON.stringify(res, null, 2), "utf8");
3834
+ const fs6 = await import("fs/promises");
3835
+ await fs6.writeFile(opts.out, JSON.stringify(res, null, 2), "utf8");
3735
3836
  spinner.succeed(`Analysis written to ${opts.out} in ${ms}ms`);
3736
3837
  } else {
3737
3838
  spinner.succeed(`Analyzed in ${ms}ms`);
@@ -3868,7 +3969,7 @@ async function explainSchemaSource(opts, out) {
3868
3969
  schema: source.schema,
3869
3970
  label: describeSchemaTarget(source.schema),
3870
3971
  config: cfg,
3871
- ...source.source === "drizzle-kit" && source.drizzleKitConfigPath ? { note: `Schema from ${path7.relative(process.cwd(), source.drizzleKitConfigPath)}` } : {}
3972
+ ...source.source === "drizzle-kit" && source.drizzleKitConfigPath ? { note: `Schema from ${path8.relative(process.cwd(), source.drizzleKitConfigPath)}` } : {}
3872
3973
  };
3873
3974
  }
3874
3975
  const detected = await detectSchema(process.cwd());
@@ -3978,7 +4079,11 @@ withOutputFlags(
3978
4079
  ).option(
3979
4080
  "--check",
3980
4081
  "regenerate and fail if the result differs from what is on disk, without changing it"
3981
- ).option("--dry-run", "report what would be written, and write nothing", false)
4082
+ ).option("--dry-run", "report what would be written, and write nothing", false).option(
4083
+ "--prune",
4084
+ "delete files a previous run wrote that this one did not, and nothing else",
4085
+ false
4086
+ )
3982
4087
  ).action(async (opts) => {
3983
4088
  const out = outputFor(opts);
3984
4089
  const planning = !!opts.check || !!opts.dryRun;
@@ -4028,7 +4133,7 @@ withOutputFlags(
4028
4133
  const n = source.schema.length;
4029
4134
  out.note(
4030
4135
  out.errStyle.gray(
4031
- `Schema from ${path7.relative(process.cwd(), source.drizzleKitConfigPath)} (${n} file${n === 1 ? "" : "s"})`
4136
+ `Schema from ${path8.relative(process.cwd(), source.drizzleKitConfigPath)} (${n} file${n === 1 ? "" : "s"})`
4032
4137
  )
4033
4138
  );
4034
4139
  }
@@ -4127,6 +4232,19 @@ withOutputFlags(
4127
4232
  files: e.files,
4128
4233
  changes: e.changes.map((c) => ({ file: displayPath(c.file), status: c.status }))
4129
4234
  }));
4235
+ const previousManifest = await readManifest(process.cwd());
4236
+ const writtenNow = plan.files.map((f) => f.file);
4237
+ const stale = await staleFiles(previousManifest, writtenNow, process.cwd());
4238
+ let stillStale = stale;
4239
+ if (opts.prune && !planning) {
4240
+ const removed = await pruneStale(process.cwd(), stale);
4241
+ for (const file of removed) out.warn(`drzl generate: removed stale ${file}`);
4242
+ const gone = new Set(removed);
4243
+ stillStale = stale.filter((s) => !gone.has(s.file));
4244
+ } else {
4245
+ const warning = staleWarning(stale);
4246
+ if (warning) warn(warning);
4247
+ }
4130
4248
  if (planning) {
4131
4249
  const wrote = await verifyNothingWasWritten(outputDirs, existing);
4132
4250
  if (wrote.length) {
@@ -4136,6 +4254,14 @@ withOutputFlags(
4136
4254
  process.exit(EXIT_FAILED);
4137
4255
  }
4138
4256
  }
4257
+ if (!planning) {
4258
+ await writeManifest(
4259
+ process.cwd(),
4260
+ nextManifestFiles(writtenNow, stillStale, process.cwd()).map(
4261
+ (f) => path8.resolve(process.cwd(), f)
4262
+ )
4263
+ );
4264
+ }
4139
4265
  if (opts.check) {
4140
4266
  const drift = pendingChanges(plan);
4141
4267
  const upToDate = drift.length === 0;
@@ -4377,10 +4503,10 @@ program.command("watch").description("Watch schema and regenerate on changes").o
4377
4503
  return;
4378
4504
  }
4379
4505
  let cfg = loaded;
4380
- const abs = (p) => path7.resolve(process.cwd(), p);
4506
+ const abs = (p) => path8.resolve(process.cwd(), p);
4381
4507
  const isInside = (child, parent) => {
4382
- const rel = path7.relative(parent, child);
4383
- return !!rel && !rel.startsWith("..") && !path7.isAbsolute(rel);
4508
+ const rel = path8.relative(parent, child);
4509
+ return !!rel && !rel.startsWith("..") && !path8.isAbsolute(rel);
4384
4510
  };
4385
4511
  let source;
4386
4512
  try {
@@ -4416,7 +4542,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
4416
4542
  if (full === dir || isInside(full, dir)) return true;
4417
4543
  }
4418
4544
  if (stats?.isDirectory()) return false;
4419
- const ext = path7.extname(full);
4545
+ const ext = path8.extname(full);
4420
4546
  if (!ext) return false;
4421
4547
  return !WATCHED_EXTENSIONS.has(ext);
4422
4548
  };
@@ -4440,6 +4566,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
4440
4566
  trigger(p);
4441
4567
  });
4442
4568
  let lastFiles = [];
4569
+ let lastSignature;
4443
4570
  const watchGenerated = (kind, files) => {
4444
4571
  if (opts.json) {
4445
4572
  out.jsonData({ event: "generate_complete", kind, files });
@@ -4526,6 +4653,13 @@ program.command("watch").description("Watch schema and regenerate on changes").o
4526
4653
  }
4527
4654
  return;
4528
4655
  }
4656
+ const signature = rebuildSignature(analysis, cfg.generators);
4657
+ if (sameAsLast(lastSignature, signature)) {
4658
+ if (opts.json) out.jsonData({ event: "generate_skipped", reason: "no change" });
4659
+ else out.note(out.errStyle.gray("No change to anything a generator reads. Skipped."));
4660
+ return;
4661
+ }
4662
+ lastSignature = signature;
4529
4663
  for (const g of selectGenerators(cfg.generators, selection.kinds)) {
4530
4664
  const entry = GENERATOR_BY_KIND.get(g.kind);
4531
4665
  if (!entry) continue;
@@ -4579,7 +4713,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
4579
4713
  } else {
4580
4714
  out.note(
4581
4715
  out.errStyle.gray(
4582
- "Watching:\n " + Array.from(currentTargets).map((p) => path7.relative(process.cwd(), p)).join("\n ")
4716
+ "Watching:\n " + Array.from(currentTargets).map((p) => path8.relative(process.cwd(), p)).join("\n ")
4583
4717
  )
4584
4718
  );
4585
4719
  }