@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.cjs CHANGED
@@ -28,7 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var import_analyzer3 = require("@drzl/analyzer");
29
29
  var import_chokidar = __toESM(require("chokidar"), 1);
30
30
  var import_commander = require("commander");
31
- var path8 = __toESM(require("path"), 1);
31
+ var path9 = __toESM(require("path"), 1);
32
32
 
33
33
  // src/output.ts
34
34
  var import_chalk = require("chalk");
@@ -2512,11 +2512,11 @@ function authTableWarnings(analysis) {
2512
2512
  var import_chalk2 = require("chalk");
2513
2513
  var PLAIN = new import_chalk2.Chalk({ level: 0 });
2514
2514
  var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
2515
- function splitPath(path9) {
2516
- if (!path9) return {};
2517
- const dot = path9.lastIndexOf(".");
2518
- if (dot <= 0) return { table: path9 };
2519
- return { table: path9.slice(0, dot), column: path9.slice(dot + 1) };
2515
+ function splitPath(path10) {
2516
+ if (!path10) return {};
2517
+ const dot = path10.lastIndexOf(".");
2518
+ if (dot <= 0) return { table: path10 };
2519
+ return { table: path10.slice(0, dot), column: path10.slice(dot + 1) };
2520
2520
  }
2521
2521
  function namedColumns(parsed) {
2522
2522
  const out = [];
@@ -3068,11 +3068,11 @@ function issueTouches(issue, table) {
3068
3068
  return dot > 0 && own.includes(issue.path.slice(0, dot));
3069
3069
  }
3070
3070
  function issueColumn(issue, table) {
3071
- const path9 = issue.path ?? "";
3071
+ const path10 = issue.path ?? "";
3072
3072
  const names = namesOf(table);
3073
3073
  for (const prefix of [names.qualified, names.tsName, names.name, table.name]) {
3074
- if (path9.startsWith(`${prefix}.`)) {
3075
- const rest = path9.slice(prefix.length + 1);
3074
+ if (path10.startsWith(`${prefix}.`)) {
3075
+ const rest = path10.slice(prefix.length + 1);
3076
3076
  if (table.columns.some((c) => c.name === rest)) return rest;
3077
3077
  }
3078
3078
  }
@@ -4231,6 +4231,107 @@ function displayPath(file, cwd = process.cwd()) {
4231
4231
  return rel && !rel.startsWith("..") ? rel : file;
4232
4232
  }
4233
4233
 
4234
+ // src/manifest.ts
4235
+ var import_node_path3 = __toESM(require("path"), 1);
4236
+ var import_node_fs3 = require("fs");
4237
+ var MANIFEST_VERSION = 1;
4238
+ var MANIFEST_PATH = import_node_path3.default.join(".drzl", "manifest.json");
4239
+ function toPosix(p) {
4240
+ return p.split(import_node_path3.default.sep).join("/");
4241
+ }
4242
+ function manifestEntries(files, root) {
4243
+ const rel = files.map((f) => toPosix(import_node_path3.default.relative(root, f)));
4244
+ return [...new Set(rel)].sort();
4245
+ }
4246
+ async function readManifest(root) {
4247
+ try {
4248
+ const raw = await import_node_fs3.promises.readFile(import_node_path3.default.join(root, MANIFEST_PATH), "utf8");
4249
+ const parsed = JSON.parse(raw);
4250
+ if (!parsed || typeof parsed !== "object" || parsed.version !== MANIFEST_VERSION || !Array.isArray(parsed.files) || !parsed.files.every((f) => typeof f === "string")) {
4251
+ return void 0;
4252
+ }
4253
+ return { version: MANIFEST_VERSION, files: parsed.files };
4254
+ } catch {
4255
+ return void 0;
4256
+ }
4257
+ }
4258
+ async function writeManifest(root, files) {
4259
+ const manifest = { version: MANIFEST_VERSION, files: manifestEntries(files, root) };
4260
+ const target = import_node_path3.default.join(root, MANIFEST_PATH);
4261
+ await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(target), { recursive: true });
4262
+ await import_node_fs3.promises.writeFile(target, `${JSON.stringify(manifest, null, 2)}
4263
+ `, "utf8");
4264
+ }
4265
+ async function staleFiles(previous, writtenNow, root) {
4266
+ if (!previous) return [];
4267
+ const current = new Set(manifestEntries(writtenNow, root));
4268
+ const gone = previous.files.filter((f) => !current.has(f));
4269
+ const out = [];
4270
+ for (const file of gone) {
4271
+ let present = true;
4272
+ try {
4273
+ await import_node_fs3.promises.access(import_node_path3.default.join(root, file));
4274
+ } catch {
4275
+ present = false;
4276
+ }
4277
+ out.push({ file, present });
4278
+ }
4279
+ return out;
4280
+ }
4281
+ async function pruneStale(root, stale) {
4282
+ const deleted = [];
4283
+ for (const entry of stale) {
4284
+ if (!entry.present) continue;
4285
+ const target = import_node_path3.default.resolve(root, entry.file);
4286
+ const inside = target === root || target.startsWith(root + import_node_path3.default.sep);
4287
+ if (!inside) continue;
4288
+ try {
4289
+ await import_node_fs3.promises.rm(target);
4290
+ deleted.push(entry.file);
4291
+ } catch {
4292
+ }
4293
+ }
4294
+ return deleted;
4295
+ }
4296
+ function staleWarning(stale) {
4297
+ const present = stale.filter((s) => s.present);
4298
+ if (!present.length) return void 0;
4299
+ const list = present.map((s) => s.file).join(", ");
4300
+ 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.`;
4301
+ }
4302
+ function nextManifestFiles(writtenNow, stale, root) {
4303
+ const kept = stale.filter((s) => s.present).map((s) => import_node_path3.default.resolve(root, s.file));
4304
+ return manifestEntries([...writtenNow, ...kept], root);
4305
+ }
4306
+
4307
+ // src/rebuild-fingerprint.ts
4308
+ var import_node_crypto = require("crypto");
4309
+ function analysisFingerprint(analysis) {
4310
+ const material = {
4311
+ dialect: analysis.dialect,
4312
+ tables: analysis.tables,
4313
+ enums: analysis.enums,
4314
+ relations: analysis.relations
4315
+ };
4316
+ return (0, import_node_crypto.createHash)("sha256").update(stableStringify(material)).digest("hex");
4317
+ }
4318
+ function configFingerprint(generators) {
4319
+ return (0, import_node_crypto.createHash)("sha256").update(stableStringify(generators)).digest("hex");
4320
+ }
4321
+ function stableStringify(value) {
4322
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
4323
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
4324
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
4325
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
4326
+ }
4327
+ function rebuildSignature(analysis, generators) {
4328
+ return { analysis: analysisFingerprint(analysis), config: configFingerprint(generators) };
4329
+ }
4330
+ function sameAsLast(previous, next) {
4331
+ if (!previous) return false;
4332
+ return previous.analysis === next.analysis && previous.config === next.config;
4333
+ }
4334
+
4234
4335
  // src/unified-diff.ts
4235
4336
  var DEFAULT_DIFF_LIMITS = {
4236
4337
  maxLines: 4e3,
@@ -4473,8 +4574,8 @@ function createRebuildScheduler(options) {
4473
4574
 
4474
4575
  // src/init.ts
4475
4576
  var import_analyzer2 = require("@drzl/analyzer");
4476
- var fs5 = __toESM(require("fs"), 1);
4477
- var path5 = __toESM(require("path"), 1);
4577
+ var fs6 = __toESM(require("fs"), 1);
4578
+ var path6 = __toESM(require("path"), 1);
4478
4579
  var INIT_GENERATOR_CHOICES = [
4479
4580
  { kind: "zod", packageName: "@drzl/generator-zod", label: "Zod validators" },
4480
4581
  { kind: "valibot", packageName: "@drzl/generator-valibot", label: "Valibot validators" },
@@ -4545,7 +4646,7 @@ async function detectSchema(cwd) {
4545
4646
  kitFiles = null;
4546
4647
  }
4547
4648
  if (kitFiles && kitPath) {
4548
- const rel = path5.relative(cwd, kitPath) || path5.basename(kitPath);
4649
+ const rel = path6.relative(cwd, kitPath) || path6.basename(kitPath);
4549
4650
  const report = await classifySchemaCandidate(kitFiles);
4550
4651
  if (report.verdict === "confirmed" || report.verdict === "unverified") {
4551
4652
  notes.push(
@@ -4561,10 +4662,10 @@ async function detectSchema(cwd) {
4561
4662
  }
4562
4663
  notes.push(`${rel} names schema files that declare no Drizzle tables; looking elsewhere.`);
4563
4664
  }
4564
- const present = schemaCandidates().filter((c) => fs5.existsSync(path5.resolve(cwd, c)));
4665
+ const present = schemaCandidates().filter((c) => fs6.existsSync(path6.resolve(cwd, c)));
4565
4666
  const unverified = [];
4566
4667
  for (const file of present) {
4567
- const report = await classifySchemaCandidate(path5.resolve(cwd, file));
4668
+ const report = await classifySchemaCandidate(path6.resolve(cwd, file));
4568
4669
  if (report.verdict === "confirmed") {
4569
4670
  notes.push(
4570
4671
  `Schema found at ${file} (${report.tables} table${report.tables === 1 ? "" : "s"})`
@@ -4666,7 +4767,7 @@ async function promptForPlan(args) {
4666
4767
  if (answer === null) endedEarly = true;
4667
4768
  else if (answer.trim()) {
4668
4769
  const typed = answer.trim();
4669
- const report = await classifySchemaCandidate(path5.resolve(cwd, typed));
4770
+ const report = await classifySchemaCandidate(path6.resolve(cwd, typed));
4670
4771
  if (report.verdict === "confirmed") {
4671
4772
  write(` ${typed}: ${report.tables} table${report.tables === 1 ? "" : "s"}`);
4672
4773
  } else if (report.verdict === "unverified") {
@@ -4728,8 +4829,8 @@ function parseGeneratorsFlag(value) {
4728
4829
  return parts;
4729
4830
  }
4730
4831
  async function runInit(args) {
4731
- const target = path5.resolve(args.cwd, "drzl.config.ts");
4732
- const existing = CONFIG_FILE_NAMES.find((name) => fs5.existsSync(path5.resolve(args.cwd, name)));
4832
+ const target = path6.resolve(args.cwd, "drzl.config.ts");
4833
+ const existing = CONFIG_FILE_NAMES.find((name) => fs6.existsSync(path6.resolve(args.cwd, name)));
4733
4834
  if (existing) {
4734
4835
  args.error(
4735
4836
  `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.`
@@ -4768,8 +4869,8 @@ async function runInit(args) {
4768
4869
  generators: fromFlag ?? [DEFAULT_GENERATOR_KIND]
4769
4870
  };
4770
4871
  if (args.schemaFlag) {
4771
- const full = path5.resolve(args.cwd, args.schemaFlag);
4772
- if (!fs5.existsSync(full)) {
4872
+ const full = path6.resolve(args.cwd, args.schemaFlag);
4873
+ if (!fs6.existsSync(full)) {
4773
4874
  args.log(`--schema ${args.schemaFlag} is not there yet. Writing it anyway.`);
4774
4875
  } else if ((await classifySchemaCandidate(full)).verdict === "rejected") {
4775
4876
  args.log(`--schema ${args.schemaFlag} declares no Drizzle tables. Writing it anyway.`);
@@ -4777,7 +4878,7 @@ async function runInit(args) {
4777
4878
  }
4778
4879
  }
4779
4880
  try {
4780
- fs5.writeFileSync(target, renderInitConfig(plan), { flag: "wx" });
4881
+ fs6.writeFileSync(target, renderInitConfig(plan), { flag: "wx" });
4781
4882
  } catch (e) {
4782
4883
  if (e?.code === "EEXIST") {
4783
4884
  args.error(
@@ -4800,10 +4901,10 @@ async function runInit(args) {
4800
4901
  }
4801
4902
 
4802
4903
  // src/sponsor.ts
4803
- var import_node_fs3 = require("fs");
4804
- var import_node_path3 = __toESM(require("path"), 1);
4805
- var CACHE_DIR = import_node_path3.default.join(process.cwd(), "node_modules", ".cache", "@drzl");
4806
- var CACHE_FILE = import_node_path3.default.join(CACHE_DIR, "sponsor-message.json");
4904
+ var import_node_fs4 = require("fs");
4905
+ var import_node_path4 = __toESM(require("path"), 1);
4906
+ var CACHE_DIR = import_node_path4.default.join(process.cwd(), "node_modules", ".cache", "@drzl");
4907
+ var CACHE_FILE = import_node_path4.default.join(CACHE_DIR, "sponsor-message.json");
4807
4908
  var DEFAULT_INTERVAL_MS = 1e3 * 60 * 15;
4808
4909
  var shownThisProcess = false;
4809
4910
  var tips = [
@@ -4827,7 +4928,7 @@ function maybeShowSponsorMessage({
4827
4928
  if (hideRequested || process.env.CI && !force || shownThisProcess && !force) return;
4828
4929
  if (!out.wantsAsides && !force) return;
4829
4930
  try {
4830
- (0, import_node_fs3.mkdirSync)(CACHE_DIR, { recursive: true });
4931
+ (0, import_node_fs4.mkdirSync)(CACHE_DIR, { recursive: true });
4831
4932
  const payload = readCache();
4832
4933
  payload.runs += 1;
4833
4934
  const now = Date.now();
@@ -4855,11 +4956,11 @@ ${green("Pro tip:")} ${tip}
4855
4956
  }
4856
4957
  }
4857
4958
  function readCache() {
4858
- if (!(0, import_node_fs3.existsSync)(CACHE_FILE)) {
4959
+ if (!(0, import_node_fs4.existsSync)(CACHE_FILE)) {
4859
4960
  return { runs: 0 };
4860
4961
  }
4861
4962
  try {
4862
- const data = JSON.parse((0, import_node_fs3.readFileSync)(CACHE_FILE, "utf8"));
4963
+ const data = JSON.parse((0, import_node_fs4.readFileSync)(CACHE_FILE, "utf8"));
4863
4964
  if (typeof data.runs !== "number") return { runs: 0 };
4864
4965
  return data;
4865
4966
  } catch {
@@ -4867,21 +4968,21 @@ function readCache() {
4867
4968
  }
4868
4969
  }
4869
4970
  function writeCache(payload) {
4870
- (0, import_node_fs3.writeFileSync)(CACHE_FILE, JSON.stringify(payload, null, 2), "utf8");
4971
+ (0, import_node_fs4.writeFileSync)(CACHE_FILE, JSON.stringify(payload, null, 2), "utf8");
4871
4972
  }
4872
4973
 
4873
4974
  // src/version.ts
4874
- var import_node_fs4 = require("fs");
4875
- var path7 = __toESM(require("path"), 1);
4975
+ var import_node_fs5 = require("fs");
4976
+ var path8 = __toESM(require("path"), 1);
4876
4977
  var import_node_url = require("url");
4877
4978
  var PACKAGE_NAME = "@drzl/cli";
4878
4979
  function moduleDir() {
4879
- return path7.dirname((0, import_node_url.fileURLToPath)(__drzlModuleUrl));
4980
+ return path8.dirname((0, import_node_url.fileURLToPath)(__drzlModuleUrl));
4880
4981
  }
4881
4982
  function readVersionFrom(manifestPath) {
4882
4983
  let raw;
4883
4984
  try {
4884
- raw = (0, import_node_fs4.readFileSync)(manifestPath, "utf8");
4985
+ raw = (0, import_node_fs5.readFileSync)(manifestPath, "utf8");
4885
4986
  } catch (e) {
4886
4987
  throw new Error(
4887
4988
  `${PACKAGE_NAME} cannot read its own version: no manifest at ${manifestPath} (${e?.message ?? String(e)}).`
@@ -4899,7 +5000,7 @@ function readVersionFrom(manifestPath) {
4899
5000
  return manifest.version;
4900
5001
  }
4901
5002
  function readCliVersion() {
4902
- return readVersionFrom(path7.join(moduleDir(), "..", "package.json"));
5003
+ return readVersionFrom(path8.join(moduleDir(), "..", "package.json"));
4903
5004
  }
4904
5005
  var CLI_VERSION = readCliVersion();
4905
5006
 
@@ -4988,8 +5089,8 @@ withOutputFlags(
4988
5089
  const errors = res.issues.some((i) => i.level === "error");
4989
5090
  const code = unreadable ? EXIT_FAILED : errors ? EXIT_FINDINGS : EXIT_OK;
4990
5091
  if (opts.out && !opts.json) {
4991
- const fs6 = await import("fs/promises");
4992
- await fs6.writeFile(opts.out, JSON.stringify(res, null, 2), "utf8");
5092
+ const fs7 = await import("fs/promises");
5093
+ await fs7.writeFile(opts.out, JSON.stringify(res, null, 2), "utf8");
4993
5094
  spinner.succeed(`Analysis written to ${opts.out} in ${ms}ms`);
4994
5095
  } else {
4995
5096
  spinner.succeed(`Analyzed in ${ms}ms`);
@@ -5126,7 +5227,7 @@ async function explainSchemaSource(opts, out) {
5126
5227
  schema: source.schema,
5127
5228
  label: describeSchemaTarget(source.schema),
5128
5229
  config: cfg,
5129
- ...source.source === "drizzle-kit" && source.drizzleKitConfigPath ? { note: `Schema from ${path8.relative(process.cwd(), source.drizzleKitConfigPath)}` } : {}
5230
+ ...source.source === "drizzle-kit" && source.drizzleKitConfigPath ? { note: `Schema from ${path9.relative(process.cwd(), source.drizzleKitConfigPath)}` } : {}
5130
5231
  };
5131
5232
  }
5132
5233
  const detected = await detectSchema(process.cwd());
@@ -5236,7 +5337,11 @@ withOutputFlags(
5236
5337
  ).option(
5237
5338
  "--check",
5238
5339
  "regenerate and fail if the result differs from what is on disk, without changing it"
5239
- ).option("--dry-run", "report what would be written, and write nothing", false)
5340
+ ).option("--dry-run", "report what would be written, and write nothing", false).option(
5341
+ "--prune",
5342
+ "delete files a previous run wrote that this one did not, and nothing else",
5343
+ false
5344
+ )
5240
5345
  ).action(async (opts) => {
5241
5346
  const out = outputFor(opts);
5242
5347
  const planning = !!opts.check || !!opts.dryRun;
@@ -5286,7 +5391,7 @@ withOutputFlags(
5286
5391
  const n = source.schema.length;
5287
5392
  out.note(
5288
5393
  out.errStyle.gray(
5289
- `Schema from ${path8.relative(process.cwd(), source.drizzleKitConfigPath)} (${n} file${n === 1 ? "" : "s"})`
5394
+ `Schema from ${path9.relative(process.cwd(), source.drizzleKitConfigPath)} (${n} file${n === 1 ? "" : "s"})`
5290
5395
  )
5291
5396
  );
5292
5397
  }
@@ -5385,6 +5490,19 @@ withOutputFlags(
5385
5490
  files: e.files,
5386
5491
  changes: e.changes.map((c) => ({ file: displayPath(c.file), status: c.status }))
5387
5492
  }));
5493
+ const previousManifest = await readManifest(process.cwd());
5494
+ const writtenNow = plan.files.map((f) => f.file);
5495
+ const stale = await staleFiles(previousManifest, writtenNow, process.cwd());
5496
+ let stillStale = stale;
5497
+ if (opts.prune && !planning) {
5498
+ const removed = await pruneStale(process.cwd(), stale);
5499
+ for (const file of removed) out.warn(`drzl generate: removed stale ${file}`);
5500
+ const gone = new Set(removed);
5501
+ stillStale = stale.filter((s) => !gone.has(s.file));
5502
+ } else {
5503
+ const warning = staleWarning(stale);
5504
+ if (warning) warn(warning);
5505
+ }
5388
5506
  if (planning) {
5389
5507
  const wrote = await verifyNothingWasWritten(outputDirs, existing);
5390
5508
  if (wrote.length) {
@@ -5394,6 +5512,14 @@ withOutputFlags(
5394
5512
  process.exit(EXIT_FAILED);
5395
5513
  }
5396
5514
  }
5515
+ if (!planning) {
5516
+ await writeManifest(
5517
+ process.cwd(),
5518
+ nextManifestFiles(writtenNow, stillStale, process.cwd()).map(
5519
+ (f) => path9.resolve(process.cwd(), f)
5520
+ )
5521
+ );
5522
+ }
5397
5523
  if (opts.check) {
5398
5524
  const drift = pendingChanges(plan);
5399
5525
  const upToDate = drift.length === 0;
@@ -5635,10 +5761,10 @@ program.command("watch").description("Watch schema and regenerate on changes").o
5635
5761
  return;
5636
5762
  }
5637
5763
  let cfg = loaded;
5638
- const abs = (p) => path8.resolve(process.cwd(), p);
5764
+ const abs = (p) => path9.resolve(process.cwd(), p);
5639
5765
  const isInside = (child, parent) => {
5640
- const rel = path8.relative(parent, child);
5641
- return !!rel && !rel.startsWith("..") && !path8.isAbsolute(rel);
5766
+ const rel = path9.relative(parent, child);
5767
+ return !!rel && !rel.startsWith("..") && !path9.isAbsolute(rel);
5642
5768
  };
5643
5769
  let source;
5644
5770
  try {
@@ -5674,7 +5800,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
5674
5800
  if (full === dir || isInside(full, dir)) return true;
5675
5801
  }
5676
5802
  if (stats?.isDirectory()) return false;
5677
- const ext = path8.extname(full);
5803
+ const ext = path9.extname(full);
5678
5804
  if (!ext) return false;
5679
5805
  return !WATCHED_EXTENSIONS.has(ext);
5680
5806
  };
@@ -5698,6 +5824,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
5698
5824
  trigger(p);
5699
5825
  });
5700
5826
  let lastFiles = [];
5827
+ let lastSignature;
5701
5828
  const watchGenerated = (kind, files) => {
5702
5829
  if (opts.json) {
5703
5830
  out.jsonData({ event: "generate_complete", kind, files });
@@ -5784,6 +5911,13 @@ program.command("watch").description("Watch schema and regenerate on changes").o
5784
5911
  }
5785
5912
  return;
5786
5913
  }
5914
+ const signature = rebuildSignature(analysis, cfg.generators);
5915
+ if (sameAsLast(lastSignature, signature)) {
5916
+ if (opts.json) out.jsonData({ event: "generate_skipped", reason: "no change" });
5917
+ else out.note(out.errStyle.gray("No change to anything a generator reads. Skipped."));
5918
+ return;
5919
+ }
5920
+ lastSignature = signature;
5787
5921
  for (const g of selectGenerators(cfg.generators, selection.kinds)) {
5788
5922
  const entry = GENERATOR_BY_KIND.get(g.kind);
5789
5923
  if (!entry) continue;
@@ -5837,7 +5971,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
5837
5971
  } else {
5838
5972
  out.note(
5839
5973
  out.errStyle.gray(
5840
- "Watching:\n " + Array.from(currentTargets).map((p) => path8.relative(process.cwd(), p)).join("\n ")
5974
+ "Watching:\n " + Array.from(currentTargets).map((p) => path9.relative(process.cwd(), p)).join("\n ")
5841
5975
  )
5842
5976
  );
5843
5977
  }