@drzl/cli 0.2.0 → 0.3.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
@@ -24,14 +24,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
- var import_commander = require("commander");
27
+ var import_analyzer = require("@drzl/analyzer");
28
+ var import_generator_orpc = require("@drzl/generator-orpc");
28
29
  var import_chalk = __toESM(require("chalk"), 1);
29
- var import_ora = __toESM(require("ora"), 1);
30
+ var import_chokidar = __toESM(require("chokidar"), 1);
30
31
  var import_cli_progress = __toESM(require("cli-progress"), 1);
32
+ var import_commander = require("commander");
33
+ var path2 = __toESM(require("path"), 1);
34
+ var import_ora = __toESM(require("ora"), 1);
31
35
 
32
36
  // src/config.ts
37
+ var fs = __toESM(require("fs"), 1);
38
+ var import_node_module = require("module");
39
+ var path = __toESM(require("path"), 1);
33
40
  var import_zod = require("zod");
34
- var import_meta = {};
35
41
  var NamingSchema = import_zod.z.object({
36
42
  routerSuffix: import_zod.z.string().default("Router"),
37
43
  procedureCase: import_zod.z.enum(["camel", "kebab", "snake"]).default("camel")
@@ -55,7 +61,7 @@ var GeneratorSchema = import_zod.z.object({
55
61
  dataAccess: import_zod.z.enum(["stub", "drizzle"]).default("stub").optional(),
56
62
  dbImportPath: import_zod.z.string().optional(),
57
63
  schemaImportPath: import_zod.z.string().optional(),
58
- // zod generator specific options
64
+ // zod/valibot/arktype generator specific options
59
65
  schemaSuffix: import_zod.z.string().optional(),
60
66
  fileSuffix: import_zod.z.string().optional(),
61
67
  // orpc validation sharing
@@ -84,49 +90,99 @@ var ConfigSchema = import_zod.z.object({
84
90
  generators: import_zod.z.array(GeneratorSchema).min(1).default([{ kind: "orpc" }])
85
91
  });
86
92
  async function loadConfig(customPath) {
87
- const path = await import("path");
88
- const fs = await import("fs/promises");
89
- const candidates = customPath ? [customPath] : ["drzl.config.ts", "drzl.config.mjs", "drzl.config.js", "drzl.config.json"];
93
+ const fsp = await import("fs/promises");
94
+ const candidates = customPath ? [customPath] : [
95
+ "drzl.config.ts",
96
+ "drzl.config.mjs",
97
+ "drzl.config.js",
98
+ "drzl.config.cjs",
99
+ "drzl.config.json"
100
+ ];
90
101
  for (const c of candidates) {
91
102
  const p = path.resolve(process.cwd(), c);
92
103
  try {
93
- await fs.access(p);
104
+ await fsp.access(p);
94
105
  } catch {
95
106
  continue;
96
107
  }
97
- if (/\.json$/i.test(p)) {
98
- try {
99
- const content = await fs.readFile(p, "utf8");
100
- const raw = JSON.parse(content);
101
- return ConfigSchema.parse(raw);
102
- } catch (e2) {
103
- throw new Error(`Failed to load config from ${p}: ${String(e2)}`);
104
- }
108
+ const ext = path.extname(p).toLowerCase();
109
+ if (ext === ".json") {
110
+ const raw2 = JSON.parse(await fsp.readFile(p, "utf8"));
111
+ return ConfigSchema.parse(raw2);
105
112
  }
113
+ const { createJiti } = await import("jiti");
114
+ const stat = await fsp.stat(p);
115
+ const base = typeof __filename !== "undefined" ? __filename : path.join(process.cwd(), "index.js");
116
+ const jiti = createJiti(base, {
117
+ moduleCache: false,
118
+ // re-evaluate each time
119
+ fsCache: true,
120
+ // keep transform cache
121
+ cacheVersion: String(stat.mtimeMs),
122
+ // bump on edit
123
+ interopDefault: true,
124
+ tryNative: false
125
+ // <— prevent native import of .ts
126
+ // debug: true,
127
+ });
128
+ const mod = await jiti.import(p);
129
+ const raw = mod?.default ?? mod;
130
+ return ConfigSchema.parse(raw);
131
+ }
132
+ return null;
133
+ }
134
+ function computeGeneratorOutputDirs(cfg, cwd = process.cwd()) {
135
+ const abs = (p) => path.resolve(cwd, p);
136
+ const dirs = /* @__PURE__ */ new Set();
137
+ dirs.add(abs(cfg.outDir));
138
+ for (const g of cfg.generators) {
139
+ if (g.kind === "service") dirs.add(abs(g.path ?? "src/services"));
140
+ if (g.kind === "zod") dirs.add(abs(g.path ?? "src/validators/zod"));
141
+ if (g.kind === "valibot") dirs.add(abs(g.path ?? "src/validators/valibot"));
142
+ if (g.kind === "arktype") dirs.add(abs(g.path ?? "src/validators/arktype"));
143
+ }
144
+ return [...dirs];
145
+ }
146
+ function resolveTemplateDirsSync(cfg, cwd = process.cwd()) {
147
+ const results = [];
148
+ const req = (0, import_node_module.createRequire)(
149
+ typeof __filename !== "undefined" ? __filename : path.join(process.cwd(), "index.js")
150
+ );
151
+ for (const g of cfg.generators) {
152
+ const t = g.template;
153
+ if (!t || t === "standard" || t === "minimal") continue;
154
+ let pkgDir = null;
106
155
  try {
107
- const { createJiti } = await import("jiti");
108
- const jit = createJiti(import_meta.url);
109
- const mod = await jit.import(p);
110
- const raw = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
111
- return ConfigSchema.parse(raw);
112
- } catch (e) {
113
- console.error(`jiti failed to load ${p}:`, e);
114
- try {
115
- const content = await fs.readFile(p, "utf8");
116
- const raw = JSON.parse(content);
117
- return ConfigSchema.parse(raw);
118
- } catch (e2) {
119
- throw new Error(`Failed to load config from ${p}: ${String(e2)}`);
120
- }
156
+ const pkg = req.resolve(`${t}/package.json`, { paths: [cwd] });
157
+ pkgDir = path.dirname(pkg);
158
+ } catch {
159
+ }
160
+ if (pkgDir) {
161
+ results.push(pkgDir);
162
+ continue;
163
+ }
164
+ if (/[./\\]/.test(t)) {
165
+ const abs = path.resolve(cwd, t);
166
+ if (fs.existsSync(abs)) results.push(abs);
121
167
  }
122
168
  }
123
- return null;
169
+ return Array.from(new Set(results));
170
+ }
171
+ function computeWatchTargets(cfg, cwd = process.cwd()) {
172
+ const abs = (p) => path.resolve(cwd, p);
173
+ const schemaAbs = abs(cfg.schema);
174
+ const targets = /* @__PURE__ */ new Set([
175
+ path.join(path.dirname(schemaAbs), "**/*.{ts,tsx,js}"),
176
+ abs("drzl.config.ts"),
177
+ abs("drzl.config.js"),
178
+ abs("drzl.config.mjs"),
179
+ abs("drzl.config.cjs")
180
+ ]);
181
+ for (const t of resolveTemplateDirsSync(cfg, cwd)) targets.add(t);
182
+ return [...targets];
124
183
  }
125
184
 
126
185
  // src/cli.ts
127
- var import_analyzer = require("@drzl/analyzer");
128
- var import_generator_orpc = require("@drzl/generator-orpc");
129
- var import_chokidar = __toESM(require("chokidar"), 1);
130
186
  var program = new import_commander.Command();
131
187
  program.name("drzl").description("DRZL - Drizzle Developer Toolkit").version("0.0.1");
132
188
  program.command("analyze").argument("<schema>", "path to drizzle schema (TS)").option("--relations", "include relations", true).option("--validate", "validate constraints", true).option("--out <file>", "write analysis JSON to file").option("--json", "print JSON to stdout (overrides --out)", false).action(async (schema, opts) => {
@@ -143,8 +199,8 @@ program.command("analyze").argument("<schema>", "path to drizzle schema (TS)").o
143
199
  if (opts.json) {
144
200
  console.log(json);
145
201
  } else if (opts.out) {
146
- const fs = await import("fs/promises");
147
- await fs.writeFile(opts.out, json, "utf8");
202
+ const fs2 = await import("fs/promises");
203
+ await fs2.writeFile(opts.out, json, "utf8");
148
204
  spinner?.succeed(import_chalk.default.green(`Analysis written to ${opts.out} in ${ms}ms`));
149
205
  } else {
150
206
  spinner?.succeed(import_chalk.default.green(`Analyzed in ${ms}ms`));
@@ -333,21 +389,84 @@ program.command("generate:orpc").argument("<schema>", "path to drizzle schema (T
333
389
  process.exit(1);
334
390
  }
335
391
  });
336
- program.command("watch").description("Watch schema and regenerate on changes").option("-c, --config <path>", "path to drzl.config").option("--pipeline <name>", "all | analyze | generate-orpc", "all").option("--debounce <ms>", "debounce ms", "200").option("--json", "emit JSON logs", false).action(async (opts) => {
337
- const cfg = await loadConfig(opts.config);
392
+ program.command("watch").description("Watch schema and regenerate on changes").option("-c, --config <path>", "path to drzl.config").option("--pipeline <name>", "all | analyze | generate-orpc", "all").option("--debounce <ms>", "debounce ms", "200").option("--json", "emit JSON logs", false).option("--poll", "force polling (helps WSL/Docker/remote FS)", false).action(async (opts) => {
393
+ let cfg = await loadConfig(opts.config);
338
394
  if (!cfg) {
339
395
  console.error(import_chalk.default.red("No config found. Create drzl.config.ts or pass --config."));
340
396
  process.exit(2);
341
397
  return;
342
398
  }
399
+ const abs = (p) => path2.resolve(process.cwd(), p);
400
+ const isInside = (child, parent) => {
401
+ const rel = path2.relative(parent, child);
402
+ return !!rel && !rel.startsWith("..") && !path2.isAbsolute(rel);
403
+ };
404
+ const ignoredOutDirs = new Set(computeGeneratorOutputDirs(cfg).map(abs));
405
+ const currentTargets = new Set(computeWatchTargets(cfg).map(abs));
406
+ const syncWatcherTargets = (watcher2, next) => {
407
+ const add = [];
408
+ const del = [];
409
+ for (const p of next) if (!currentTargets.has(p)) add.push(p);
410
+ for (const p of currentTargets) if (!next.has(p)) del.push(p);
411
+ if (add.length) watcher2.add(add);
412
+ if (del.length) watcher2.unwatch(del);
413
+ currentTargets.clear();
414
+ next.forEach((p) => currentTargets.add(p));
415
+ };
416
+ const rebuildIgnoreDirsFrom = (cfgNow) => {
417
+ ignoredOutDirs.clear();
418
+ for (const d of computeGeneratorOutputDirs(cfgNow)) ignoredOutDirs.add(abs(d));
419
+ };
420
+ const ignoredFn = (p) => {
421
+ const full = abs(p);
422
+ for (const dir of ignoredOutDirs) {
423
+ if (full === dir || isInside(full, dir)) return true;
424
+ }
425
+ return false;
426
+ };
427
+ const watcher = import_chokidar.default.watch(Array.from(currentTargets), {
428
+ ignoreInitial: true,
429
+ awaitWriteFinish: { stabilityThreshold: 400, pollInterval: 50 },
430
+ usePolling: !!opts.poll,
431
+ ignored: ignoredFn
432
+ });
433
+ const logTrigger = (type, file) => {
434
+ if (opts.json) console.log(JSON.stringify({ event: "trigger", type, file }));
435
+ };
436
+ watcher.on("add", (p) => {
437
+ logTrigger("add", p);
438
+ trigger(p);
439
+ }).on("change", (p) => {
440
+ logTrigger("change", p);
441
+ trigger(p);
442
+ }).on("unlink", (p) => {
443
+ logTrigger("unlink", p);
444
+ trigger(p);
445
+ });
343
446
  let lastFiles = [];
344
447
  const run = async () => {
345
448
  try {
449
+ const reloaded = await loadConfig(opts.config);
450
+ if (!reloaded) throw new Error("Config disappeared during watch.");
451
+ cfg = reloaded;
452
+ rebuildIgnoreDirsFrom(cfg);
453
+ const nextTargets = new Set(computeWatchTargets(cfg).map(abs));
454
+ syncWatcherTargets(watcher, nextTargets);
346
455
  if (!opts.json) console.clear();
456
+ if (opts.json) {
457
+ console.log(
458
+ JSON.stringify({
459
+ event: "watch_config_applied",
460
+ targets: Array.from(currentTargets),
461
+ ignored: Array.from(ignoredOutDirs)
462
+ })
463
+ );
464
+ }
347
465
  const analyzer = new import_analyzer.SchemaAnalyzer(cfg.schema);
348
466
  const analysis = await analyzer.analyze({
349
467
  includeRelations: cfg.analyzer.includeRelations,
350
- validateConstraints: cfg.analyzer.validateConstraints
468
+ validateConstraints: cfg.analyzer.validateConstraints,
469
+ includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
351
470
  });
352
471
  if (opts.pipeline === "analyze") {
353
472
  if (opts.json) {
@@ -365,62 +484,174 @@ program.command("watch").description("Watch schema and regenerate on changes").o
365
484
  }
366
485
  const newFiles = [];
367
486
  for (const g of cfg.generators) {
368
- if (g.kind === "orpc" && (opts.pipeline === "all" || opts.pipeline === "generate-orpc")) {
487
+ if (opts.pipeline !== "all" && !(opts.pipeline === "generate-orpc" && g.kind === "orpc")) {
488
+ continue;
489
+ }
490
+ if (g.kind === "orpc") {
369
491
  const gen = new import_generator_orpc.ORPCGenerator(analysis);
370
492
  const { files } = await gen.generate({
371
493
  outputDir: cfg.outDir,
372
494
  template: g.template,
373
495
  includeRelations: g.includeRelations,
374
- naming: g.naming
496
+ naming: g.naming,
497
+ outputHeader: g.outputHeader,
498
+ format: g.format,
499
+ templateOptions: g.templateOptions,
500
+ validation: g.validation
375
501
  });
376
- if (opts.json) {
377
- console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files }));
378
- } else {
379
- console.log(
380
- import_chalk.default.green(`Generated (${g.kind}):`),
502
+ opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
503
+ import_chalk.default.green(`Generated (${g.kind}):`),
504
+ files.map((f) => import_chalk.default.cyan(f)).join(", ")
505
+ );
506
+ newFiles.push(...files);
507
+ } else if (g.kind === "service") {
508
+ try {
509
+ const { ServiceGenerator } = await import("@drzl/generator-service");
510
+ const gen = new ServiceGenerator(analysis);
511
+ const target = g.path ?? "src/services";
512
+ const files = await gen.generate({
513
+ outDir: target,
514
+ outputHeader: g.outputHeader,
515
+ format: g.format,
516
+ dataAccess: g.dataAccess,
517
+ dbImportPath: g.dbImportPath,
518
+ schemaImportPath: g.schemaImportPath
519
+ });
520
+ opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
521
+ import_chalk.default.green(`Generated (service): ${files.length} files`),
381
522
  files.map((f) => import_chalk.default.cyan(f)).join(", ")
382
523
  );
524
+ newFiles.push(...files);
525
+ } catch (e) {
526
+ console.error(
527
+ import_chalk.default.red("Service generator missing."),
528
+ import_chalk.default.yellow("\nInstall with: npm install @drzl/generator-service")
529
+ );
530
+ console.error(import_chalk.default.gray("Error details:"), e?.message ?? e);
531
+ return;
532
+ }
533
+ } else if (g.kind === "zod") {
534
+ try {
535
+ const { ZodGenerator } = await import("@drzl/generator-zod");
536
+ const gen = new ZodGenerator(analysis);
537
+ const target = g.path ?? "src/validators/zod";
538
+ const files = await gen.generate({
539
+ outDir: target,
540
+ outputHeader: g.outputHeader,
541
+ format: g.format,
542
+ schemaSuffix: g.schemaSuffix,
543
+ fileSuffix: g.fileSuffix
544
+ });
545
+ opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
546
+ import_chalk.default.green(`Generated (zod): ${files.length} files`),
547
+ files.map((f) => import_chalk.default.cyan(f)).join(", ")
548
+ );
549
+ newFiles.push(...files);
550
+ } catch (e) {
551
+ console.error(
552
+ import_chalk.default.red("Zod generator missing."),
553
+ import_chalk.default.yellow("\nInstall with: npm install @drzl/generator-zod")
554
+ );
555
+ console.error(import_chalk.default.gray("Error details:"), e?.message ?? e);
556
+ return;
557
+ }
558
+ } else if (g.kind === "valibot") {
559
+ try {
560
+ const { ValibotGenerator } = await import("@drzl/generator-valibot");
561
+ const gen = new ValibotGenerator(analysis);
562
+ const target = g.path ?? "src/validators/valibot";
563
+ const files = await gen.generate({
564
+ outDir: target,
565
+ outputHeader: g.outputHeader,
566
+ format: g.format,
567
+ schemaSuffix: g.schemaSuffix,
568
+ fileSuffix: g.fileSuffix
569
+ });
570
+ opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
571
+ import_chalk.default.green(`Generated (valibot): ${files.length} files`),
572
+ files.map((f) => import_chalk.default.cyan(f)).join(", ")
573
+ );
574
+ newFiles.push(...files);
575
+ } catch (e) {
576
+ console.error(
577
+ import_chalk.default.red("Valibot generator missing."),
578
+ import_chalk.default.yellow("\nInstall with: npm install @drzl/generator-valibot")
579
+ );
580
+ console.error(import_chalk.default.gray("Error details:"), e?.message ?? e);
581
+ return;
582
+ }
583
+ } else if (g.kind === "arktype") {
584
+ try {
585
+ const { ArkTypeGenerator } = await import("@drzl/generator-arktype");
586
+ const gen = new ArkTypeGenerator(analysis);
587
+ const target = g.path ?? "src/validators/arktype";
588
+ const files = await gen.generate({
589
+ outDir: target,
590
+ outputHeader: g.outputHeader,
591
+ format: g.format,
592
+ schemaSuffix: g.schemaSuffix,
593
+ fileSuffix: g.fileSuffix
594
+ });
595
+ opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
596
+ import_chalk.default.green(`Generated (arktype): ${files.length} files`),
597
+ files.map((f) => import_chalk.default.cyan(f)).join(", ")
598
+ );
599
+ newFiles.push(...files);
600
+ } catch (e) {
601
+ console.error(
602
+ import_chalk.default.red("ArkType generator missing."),
603
+ import_chalk.default.yellow("\nInstall with: npm install @drzl/generator-arktype")
604
+ );
605
+ console.error(import_chalk.default.gray("Error details:"), e?.message ?? e);
606
+ return;
383
607
  }
384
- newFiles.push(...files);
385
608
  }
386
609
  }
387
610
  const added = newFiles.filter((f) => !lastFiles.includes(f));
388
611
  const removed = lastFiles.filter((f) => !newFiles.includes(f));
389
- if (opts.json) {
390
- console.log(JSON.stringify({ event: "diff", added, removed }));
391
- } else {
612
+ opts.json ? console.log(JSON.stringify({ event: "diff", added, removed })) : (() => {
392
613
  if (added.length) console.log(import_chalk.default.blue(`Added: ${added.join(", ")}`));
393
614
  if (removed.length) console.log(import_chalk.default.yellow(`Removed: ${removed.join(", ")}`));
394
- }
615
+ })();
395
616
  lastFiles = newFiles;
396
617
  } catch (e) {
397
- if (opts.json)
398
- console.log(JSON.stringify({ event: "error", message: String(e?.message ?? e) }));
399
- else console.error(import_chalk.default.red("Watch pipeline failed:"), e?.message ?? e);
618
+ opts.json ? console.log(JSON.stringify({ event: "error", message: String(e?.message ?? e) })) : console.error(import_chalk.default.red("Watch pipeline failed:"), e?.message ?? e);
400
619
  }
401
620
  };
402
621
  const debounced = Number(opts.debounce) || 200;
403
622
  let timer = null;
404
623
  const trigger = (file) => {
405
- if (file && file.startsWith(cfg.outDir)) return;
406
- clearTimeout(timer);
624
+ if (file) {
625
+ const full = abs(file);
626
+ for (const dir of ignoredOutDirs) {
627
+ if (full === dir || isInside(full, dir)) return;
628
+ }
629
+ }
630
+ if (timer) clearTimeout(timer);
407
631
  timer = setTimeout(run, debounced);
408
632
  };
409
- console.log(import_chalk.default.gray("Watching for changes..."));
410
- const watchSet = /* @__PURE__ */ new Set([cfg.schema]);
411
- for (const g of cfg.generators) {
412
- if (g.template && g.template !== "standard" && g.template !== "minimal") {
413
- watchSet.add(g.template);
414
- }
633
+ if (opts.json) {
634
+ console.log(
635
+ JSON.stringify({
636
+ event: "watching",
637
+ targets: Array.from(currentTargets),
638
+ ignored: Array.from(ignoredOutDirs)
639
+ })
640
+ );
641
+ } else {
642
+ console.log(
643
+ import_chalk.default.gray(
644
+ "Watching:\n " + Array.from(currentTargets).map((p) => path2.relative(process.cwd(), p)).join("\n ")
645
+ )
646
+ );
415
647
  }
416
- watchSet.add(cfg.outDir);
417
- import_chokidar.default.watch(Array.from(watchSet), { ignoreInitial: true }).on("add", (p) => trigger(p)).on("change", (p) => trigger(p)).on("unlink", (p) => trigger(p));
648
+ watcher.on("add", (p) => trigger(p)).on("change", (p) => trigger(p)).on("unlink", (p) => trigger(p)).on("error", (err) => console.error(import_chalk.default.red("Watcher error:"), err));
418
649
  await run();
419
650
  });
420
651
  program.command("init").description("Scaffold a drzl.config.ts").option("-y, --yes", "accept defaults").action(async (_opts) => {
421
- const fs = await import("fs/promises");
422
- const path = await import("path");
423
- const target = path.resolve(process.cwd(), "drzl.config.ts");
652
+ const fs2 = await import("fs/promises");
653
+ const path3 = await import("path");
654
+ const target = path3.resolve(process.cwd(), "drzl.config.ts");
424
655
  const template = `export default {
425
656
  schema: 'src/db/schema.ts',
426
657
  outDir: 'src/api',
@@ -431,7 +662,7 @@ program.command("init").description("Scaffold a drzl.config.ts").option("-y, --y
431
662
  } as const
432
663
  `;
433
664
  try {
434
- await fs.writeFile(target, template, { flag: "wx" });
665
+ await fs2.writeFile(target, template, { flag: "wx" });
435
666
  console.log(import_chalk.default.green(`Created ${target}`));
436
667
  } catch (e) {
437
668
  console.error(import_chalk.default.red("Init failed:"), e?.message ?? e);