@drzl/cli 4.21.0 → 4.24.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/{chunk-XNNKHBGV.js → chunk-54E2IO7N.js} +491 -65
- package/dist/chunk-54E2IO7N.js.map +1 -0
- package/dist/cli.cjs +3359 -2491
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2860 -698
- package/dist/cli.js.map +1 -1
- package/dist/config.cjs +497 -64
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +264 -8
- package/dist/config.d.ts +264 -8
- package/dist/config.js +25 -1
- package/dist/drzl.config.schema.json +717 -0
- package/package.json +19 -15
- package/dist/chunk-XNNKHBGV.js.map +0 -1
- package/dist/dist-K6N4F3XW.js +0 -612
- package/dist/dist-K6N4F3XW.js.map +0 -1
- package/dist/dist-WHAW3AMQ.js +0 -585
- package/dist/dist-WHAW3AMQ.js.map +0 -1
- package/dist/dist-XBGVORL3.js +0 -512
- package/dist/dist-XBGVORL3.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,28 +1,290 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CONFIG_FILE_NAMES,
|
|
4
|
+
ConfigValidationError,
|
|
5
|
+
GENERATOR_KINDS,
|
|
3
6
|
addressableName,
|
|
4
7
|
ambiguousPatternWarnings,
|
|
5
8
|
computeGeneratorOutputDirs,
|
|
6
9
|
computeWatchTargets,
|
|
10
|
+
configFromKinds,
|
|
7
11
|
displayTableName,
|
|
12
|
+
expressOutDir,
|
|
13
|
+
fastifyOutDir,
|
|
8
14
|
filterTables,
|
|
15
|
+
graphqlOutDir,
|
|
9
16
|
hasNamedSchemas,
|
|
17
|
+
honoOutDir,
|
|
18
|
+
importFreshConfigModule,
|
|
10
19
|
loadConfig,
|
|
11
20
|
matchesAny,
|
|
12
21
|
matchesTable,
|
|
22
|
+
nearestKey,
|
|
23
|
+
nestjsOutDir,
|
|
24
|
+
tableAliases,
|
|
13
25
|
tableFilterWarnings,
|
|
14
26
|
trpcOutDir
|
|
15
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-54E2IO7N.js";
|
|
16
28
|
|
|
17
29
|
// src/cli.ts
|
|
18
|
-
import { SchemaAnalyzer } from "@drzl/analyzer";
|
|
19
|
-
import { ORPCGenerator } from "@drzl/generator-orpc";
|
|
20
|
-
import chalk3 from "chalk";
|
|
30
|
+
import { qualifiedTableName as qualifiedTableName2, SchemaAnalyzer as SchemaAnalyzer2 } from "@drzl/analyzer";
|
|
21
31
|
import chokidar from "chokidar";
|
|
22
|
-
import cliProgress from "cli-progress";
|
|
23
32
|
import { Command } from "commander";
|
|
24
|
-
import * as
|
|
33
|
+
import * as path7 from "path";
|
|
34
|
+
|
|
35
|
+
// src/output.ts
|
|
36
|
+
import { Chalk } from "chalk";
|
|
37
|
+
import cliProgress from "cli-progress";
|
|
25
38
|
import ora from "ora";
|
|
39
|
+
var EXIT_OK = 0;
|
|
40
|
+
var EXIT_FAILED = 1;
|
|
41
|
+
var EXIT_FINDINGS = 2;
|
|
42
|
+
var PROGRESS_MIN_TABLES = 25;
|
|
43
|
+
function colorLevelFor(stream, env) {
|
|
44
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return 0;
|
|
45
|
+
if (env.TERM === "dumb") return 0;
|
|
46
|
+
const forced = env.FORCE_COLOR;
|
|
47
|
+
if (forced !== void 0) {
|
|
48
|
+
if (forced === "false" || forced === "0") return 0;
|
|
49
|
+
if (forced === "" || forced === "true") return 1;
|
|
50
|
+
const n = Number.parseInt(forced, 10);
|
|
51
|
+
if (Number.isInteger(n)) return Math.min(Math.max(n, 0), 3);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
if (!stream.isTTY) return 0;
|
|
55
|
+
if (env.COLORTERM === "truecolor" || env.COLORTERM === "24bit") return 3;
|
|
56
|
+
if (env.TERM?.includes("256")) return 2;
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
function shouldShowProgress(opts) {
|
|
60
|
+
if (opts.quiet || opts.json) return false;
|
|
61
|
+
if (!opts.stderr.isTTY) return false;
|
|
62
|
+
return opts.tables >= PROGRESS_MIN_TABLES;
|
|
63
|
+
}
|
|
64
|
+
function createProgress(enabled, total, stream) {
|
|
65
|
+
if (!enabled) {
|
|
66
|
+
return { start() {
|
|
67
|
+
}, update() {
|
|
68
|
+
}, stop() {
|
|
69
|
+
} };
|
|
70
|
+
}
|
|
71
|
+
const bar = new cliProgress.SingleBar(
|
|
72
|
+
{ hideCursor: true, stream },
|
|
73
|
+
cliProgress.Presets.shades_classic
|
|
74
|
+
);
|
|
75
|
+
let running = false;
|
|
76
|
+
return {
|
|
77
|
+
start() {
|
|
78
|
+
if (running) return;
|
|
79
|
+
bar.start(total, 0);
|
|
80
|
+
running = true;
|
|
81
|
+
},
|
|
82
|
+
update(value) {
|
|
83
|
+
if (running) bar.update(value);
|
|
84
|
+
},
|
|
85
|
+
stop() {
|
|
86
|
+
if (!running) return;
|
|
87
|
+
bar.stop();
|
|
88
|
+
running = false;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
var Output = class {
|
|
93
|
+
constructor(options = {}) {
|
|
94
|
+
this.stdout = options.stdout ?? process.stdout;
|
|
95
|
+
this.stderr = options.stderr ?? process.stderr;
|
|
96
|
+
this.env = options.env ?? process.env;
|
|
97
|
+
this.quiet = options.quiet ?? false;
|
|
98
|
+
this.json = options.json ?? false;
|
|
99
|
+
this.outStyle = new Chalk({ level: colorLevelFor(this.stdout, this.env) });
|
|
100
|
+
this.errStyle = new Chalk({ level: colorLevelFor(this.stderr, this.env) });
|
|
101
|
+
}
|
|
102
|
+
/** The command's answer. Never suppressed by `--quiet`, because then nothing would be left. */
|
|
103
|
+
data(text) {
|
|
104
|
+
this.stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The one JSON document `--json` promises, and the reason nothing else may touch stdout.
|
|
108
|
+
*
|
|
109
|
+
* Stringified without indentation on purpose: this is a machine's copy, `jq` formats it for a
|
|
110
|
+
* human, and the two commands that already print an indented document (`analyze`, `doctor`) keep
|
|
111
|
+
* doing so through `data` because their shape is a published contract.
|
|
112
|
+
*/
|
|
113
|
+
jsonData(payload) {
|
|
114
|
+
this.data(JSON.stringify(payload));
|
|
115
|
+
}
|
|
116
|
+
/** Narration. Dropped by `--quiet` and by `--json`. */
|
|
117
|
+
note(text) {
|
|
118
|
+
if (this.quiet || this.json) return;
|
|
119
|
+
this.stderr.write(text + "\n");
|
|
120
|
+
}
|
|
121
|
+
/** A warning: narration a user asked to be quiet still does not need. */
|
|
122
|
+
warn(text) {
|
|
123
|
+
if (this.quiet || this.json) return;
|
|
124
|
+
this.stderr.write(this.errStyle.yellow(text) + "\n");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A failure. Never suppressed by anything, because a script that cannot tell a success from a
|
|
128
|
+
* swallowed failure is worse off than one with no `--quiet` at all.
|
|
129
|
+
*
|
|
130
|
+
* Under `--json` the machine-readable failure goes to stdout as the document, so this stays
|
|
131
|
+
* quiet there rather than printing the same fact twice in two shapes.
|
|
132
|
+
*/
|
|
133
|
+
error(text, detail) {
|
|
134
|
+
if (this.json) return;
|
|
135
|
+
const line = this.errStyle.red(text) + (detail ? " " + detail : "");
|
|
136
|
+
this.stderr.write(line + "\n");
|
|
137
|
+
}
|
|
138
|
+
/** A hint under an error. Suppressed by `--quiet`: the error above it already said what broke. */
|
|
139
|
+
hint(text) {
|
|
140
|
+
if (this.quiet || this.json) return;
|
|
141
|
+
this.stderr.write(this.errStyle.dim(text) + "\n");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A spinner on stderr, or nothing.
|
|
145
|
+
*
|
|
146
|
+
* `ora` is constructed only when stderr is a terminal. Given a pipe it still writes its text
|
|
147
|
+
* once as `- Analyzing...`, which is a line nobody reading a log wants, and given `NO_COLOR` it
|
|
148
|
+
* writes a coloured symbol anyway. Both are avoided by not building it.
|
|
149
|
+
*/
|
|
150
|
+
spinner(text) {
|
|
151
|
+
const live = !this.quiet && !this.json && this.stderr.isTTY ? ora({
|
|
152
|
+
text,
|
|
153
|
+
stream: this.stderr,
|
|
154
|
+
// ora paints its own frame cyan through its own chalk, which is a second colour
|
|
155
|
+
// decision beside this one and does not read `NO_COLOR` either. Measured with the
|
|
156
|
+
// variable set: everything else on the line went plain and the spinner frame arrived
|
|
157
|
+
// as `[36m⠋[39m`. `false` is ora's documented way to turn that off.
|
|
158
|
+
color: this.errStyle.level > 0 ? "cyan" : false
|
|
159
|
+
}).start() : null;
|
|
160
|
+
return {
|
|
161
|
+
succeed: (done) => {
|
|
162
|
+
live?.stop();
|
|
163
|
+
this.succeed(done);
|
|
164
|
+
},
|
|
165
|
+
fail: (done) => {
|
|
166
|
+
live?.stop();
|
|
167
|
+
this.error(done);
|
|
168
|
+
},
|
|
169
|
+
stop: () => live?.stop()
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A completed step.
|
|
174
|
+
*
|
|
175
|
+
* The tick is rendered here rather than by `ora.succeed`, which is the fix for the escape that
|
|
176
|
+
* reached piped output: `log-symbols` colours the symbol from the environment alone and never
|
|
177
|
+
* looks at the stream, so `drzl analyze 2> log` used to write `[32m✔[39m` into
|
|
178
|
+
* the file. Here the symbol goes through the same per-stream decision as everything else.
|
|
179
|
+
*/
|
|
180
|
+
succeed(text) {
|
|
181
|
+
if (this.quiet || this.json) return;
|
|
182
|
+
this.stderr.write(this.errStyle.green("\u2714") + " " + text + "\n");
|
|
183
|
+
}
|
|
184
|
+
/** A progress bar for `tables` items, or a no-op. See `shouldShowProgress` for the four gates. */
|
|
185
|
+
progress(tables) {
|
|
186
|
+
return createProgress(
|
|
187
|
+
shouldShowProgress({
|
|
188
|
+
tables,
|
|
189
|
+
stderr: this.stderr,
|
|
190
|
+
quiet: this.quiet,
|
|
191
|
+
json: this.json
|
|
192
|
+
}),
|
|
193
|
+
tables,
|
|
194
|
+
this.stderr
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Whether an unrequested extra, such as the sponsor tip, should be shown at all.
|
|
199
|
+
*
|
|
200
|
+
* A terminal is the only place an aside has a reader. Piped into a file it is noise in someone's
|
|
201
|
+
* log, and under `--json` it would be noise in the middle of a document.
|
|
202
|
+
*/
|
|
203
|
+
get wantsAsides() {
|
|
204
|
+
return !this.quiet && !this.json && Boolean(this.stderr.isTTY);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
function jsonFailure(command, code, message, exitCode = EXIT_FAILED) {
|
|
208
|
+
return { ok: false, command, code, message, exitCode };
|
|
209
|
+
}
|
|
210
|
+
function messageOf(value) {
|
|
211
|
+
const message = value?.message;
|
|
212
|
+
return String(message ?? value);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/express-options.ts
|
|
216
|
+
function expressOptions(g, cfg) {
|
|
217
|
+
return {
|
|
218
|
+
outputDir: expressOutDir(g, cfg),
|
|
219
|
+
includeRelations: g.includeRelations,
|
|
220
|
+
naming: g.naming,
|
|
221
|
+
outputHeader: g.outputHeader,
|
|
222
|
+
format: g.format,
|
|
223
|
+
importExtension: g.importExtension,
|
|
224
|
+
validation: g.validation
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/fastify-options.ts
|
|
229
|
+
function fastifyOptions(g, cfg) {
|
|
230
|
+
return {
|
|
231
|
+
outputDir: fastifyOutDir(g, cfg),
|
|
232
|
+
includeRelations: g.includeRelations,
|
|
233
|
+
naming: g.naming,
|
|
234
|
+
outputHeader: g.outputHeader,
|
|
235
|
+
format: g.format,
|
|
236
|
+
importExtension: g.importExtension
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/generator-loader.ts
|
|
241
|
+
var GeneratorNotInstalledError = class extends Error {
|
|
242
|
+
constructor(specifier, reason) {
|
|
243
|
+
super(`${specifier} is not installed`);
|
|
244
|
+
this.specifier = specifier;
|
|
245
|
+
this.reason = reason;
|
|
246
|
+
this.name = "GeneratorNotInstalledError";
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
function isPackageMissing(err, specifier) {
|
|
250
|
+
const code = err?.code;
|
|
251
|
+
if (code !== "ERR_MODULE_NOT_FOUND") return false;
|
|
252
|
+
const message = err?.message;
|
|
253
|
+
return typeof message === "string" && message.includes(`'${specifier}'`);
|
|
254
|
+
}
|
|
255
|
+
async function loadGenerator(specifier, load) {
|
|
256
|
+
try {
|
|
257
|
+
return await load();
|
|
258
|
+
} catch (e) {
|
|
259
|
+
if (isPackageMissing(e, specifier)) throw new GeneratorNotInstalledError(specifier, e);
|
|
260
|
+
throw e;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/graphql-options.ts
|
|
265
|
+
function graphqlOptions(g, cfg) {
|
|
266
|
+
return {
|
|
267
|
+
outputDir: graphqlOutDir(g, cfg),
|
|
268
|
+
naming: g.naming,
|
|
269
|
+
outputHeader: g.outputHeader,
|
|
270
|
+
format: g.format,
|
|
271
|
+
importExtension: g.importExtension
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/hono-options.ts
|
|
276
|
+
function honoOptions(g, cfg) {
|
|
277
|
+
return {
|
|
278
|
+
outputDir: honoOutDir(g, cfg),
|
|
279
|
+
includeRelations: g.includeRelations,
|
|
280
|
+
naming: g.naming,
|
|
281
|
+
validator: g.validator,
|
|
282
|
+
outputHeader: g.outputHeader,
|
|
283
|
+
format: g.format,
|
|
284
|
+
importExtension: g.importExtension,
|
|
285
|
+
validation: g.validation
|
|
286
|
+
};
|
|
287
|
+
}
|
|
26
288
|
|
|
27
289
|
// src/validation-options.ts
|
|
28
290
|
function validationOptions(g, cfg, outDir, caps = {}) {
|
|
@@ -53,7 +315,8 @@ function validationOptions(g, cfg, outDir, caps = {}) {
|
|
|
53
315
|
typedColumns: g.typedColumns
|
|
54
316
|
} : {},
|
|
55
317
|
...caps.standardSchema ? { standardSchema: g.standardSchema } : {},
|
|
56
|
-
...caps.meta ? { meta: g.meta } : {}
|
|
318
|
+
...caps.meta ? { meta: g.meta } : {},
|
|
319
|
+
...caps.constraints ? { constraints: g.constraints } : {}
|
|
57
320
|
};
|
|
58
321
|
}
|
|
59
322
|
|
|
@@ -67,7 +330,57 @@ function jsonSchemaOptions(g, cfg, outDir) {
|
|
|
67
330
|
document: g.document,
|
|
68
331
|
// Read only while emitting a document, where it adds `/users/{id}/posts`. The per-table
|
|
69
332
|
// schemas are flat whatever it says.
|
|
70
|
-
includeRelations: g.includeRelations
|
|
333
|
+
includeRelations: g.includeRelations,
|
|
334
|
+
// The mirror image: read only for the per-table modules, since the document shares regardless.
|
|
335
|
+
sharedEnums: g.sharedEnums
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/nestjs-options.ts
|
|
340
|
+
function nestjsOptions(g, cfg) {
|
|
341
|
+
return {
|
|
342
|
+
outputDir: nestjsOutDir(g, cfg),
|
|
343
|
+
naming: g.naming,
|
|
344
|
+
outputHeader: g.outputHeader,
|
|
345
|
+
format: g.format,
|
|
346
|
+
importExtension: g.importExtension,
|
|
347
|
+
validation: g.validation
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/orpc-options.ts
|
|
352
|
+
function orpcOptions(g, cfg, servicesDir) {
|
|
353
|
+
return {
|
|
354
|
+
outputDir: cfg.outDir,
|
|
355
|
+
template: g.template,
|
|
356
|
+
includeRelations: g.includeRelations,
|
|
357
|
+
naming: g.naming,
|
|
358
|
+
outputHeader: g.outputHeader,
|
|
359
|
+
format: g.format,
|
|
360
|
+
templateOptions: g.templateOptions,
|
|
361
|
+
importExtension: g.importExtension,
|
|
362
|
+
validation: g.validation,
|
|
363
|
+
// Documented on this generator since it was added and unreachable from a config file for most
|
|
364
|
+
// of that time, because the config schema had no such key and zod stripped it in silence.
|
|
365
|
+
databaseInjection: g.databaseInjection,
|
|
366
|
+
// Where the service generator is actually writing, so a router template that imports services
|
|
367
|
+
// spells a path that exists. The templates default this to `src/services`, which is right only
|
|
368
|
+
// by coincidence for a config that puts them elsewhere.
|
|
369
|
+
servicesDir
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/service-options.ts
|
|
374
|
+
function serviceOptions(g, outDir) {
|
|
375
|
+
return {
|
|
376
|
+
outDir,
|
|
377
|
+
outputHeader: g.outputHeader,
|
|
378
|
+
format: g.format,
|
|
379
|
+
dataAccess: g.dataAccess,
|
|
380
|
+
dbImportPath: g.dbImportPath,
|
|
381
|
+
schemaImportPath: g.schemaImportPath,
|
|
382
|
+
importExtension: g.importExtension,
|
|
383
|
+
databaseInjection: g.databaseInjection
|
|
71
384
|
};
|
|
72
385
|
}
|
|
73
386
|
|
|
@@ -90,18 +403,341 @@ function trpcOptions(g, cfg, servicesDir) {
|
|
|
90
403
|
};
|
|
91
404
|
}
|
|
92
405
|
|
|
406
|
+
// src/generator-registry.ts
|
|
407
|
+
var VALIDATOR_DEFAULT_DIRS = {
|
|
408
|
+
zod: "src/validators/zod",
|
|
409
|
+
valibot: "src/validators/valibot",
|
|
410
|
+
arktype: "src/validators/arktype",
|
|
411
|
+
typebox: "src/validators/typebox",
|
|
412
|
+
effect: "src/validators/effect",
|
|
413
|
+
"json-schema": "src/validators/json-schema"
|
|
414
|
+
};
|
|
415
|
+
var SERVICES_DEFAULT_DIR = "src/services";
|
|
416
|
+
function resolveServicesDir(cfg) {
|
|
417
|
+
return cfg.generators.find((g) => g.kind === "service")?.path ?? SERVICES_DEFAULT_DIR;
|
|
418
|
+
}
|
|
419
|
+
var GENERATORS = [
|
|
420
|
+
{
|
|
421
|
+
kind: "orpc",
|
|
422
|
+
specifier: "@drzl/generator-orpc",
|
|
423
|
+
load: () => import("@drzl/generator-orpc"),
|
|
424
|
+
construct: (m, analysis) => new m.ORPCGenerator(analysis),
|
|
425
|
+
// `cfg.outDir` and never `g.path`: see `orpcOptions` for why that is this generator's own
|
|
426
|
+
// arrangement rather than an oversight to correct here.
|
|
427
|
+
outputDir: (_g, cfg) => cfg.outDir,
|
|
428
|
+
options: (g, cfg, ctx) => orpcOptions(g, cfg, ctx.servicesDir)
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
kind: "trpc",
|
|
432
|
+
// This one and seven others were `optionalDependencies` until every one of them had been
|
|
433
|
+
// published: a package that has never existed cannot publish through npm's trusted-publisher
|
|
434
|
+
// OIDC flow, so its first version goes out by hand, and naming it as a hard dependency in the
|
|
435
|
+
// same release breaks `npm i @drzl/cli` for everyone until it does exist. An optional
|
|
436
|
+
// dependency is skipped by the installer instead, which made that release safe.
|
|
437
|
+
//
|
|
438
|
+
// The side effect was invisible and lasted longer than the reason: tsup externalises
|
|
439
|
+
// `dependencies` and `peerDependencies` and bundles everything else, so those eight travelled
|
|
440
|
+
// inside `dist` while the other six were resolved from `node_modules`. All fourteen are on the
|
|
441
|
+
// registry now and all fourteen are `dependencies`, which is what makes every one of them a
|
|
442
|
+
// package that can genuinely be absent, and `loadGenerator` tell absence apart from failure
|
|
443
|
+
// for every kind rather than for six of them.
|
|
444
|
+
specifier: "@drzl/generator-trpc",
|
|
445
|
+
load: () => import("@drzl/generator-trpc"),
|
|
446
|
+
construct: (m, analysis) => new m.TRPCGenerator(analysis),
|
|
447
|
+
outputDir: (g, cfg) => trpcOutDir(g, cfg),
|
|
448
|
+
options: (g, cfg, ctx) => trpcOptions(g, cfg, ctx.servicesDir)
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
kind: "hono",
|
|
452
|
+
specifier: "@drzl/generator-hono",
|
|
453
|
+
load: () => import("@drzl/generator-hono"),
|
|
454
|
+
construct: (m, analysis) => new m.HonoGenerator(analysis),
|
|
455
|
+
outputDir: (g, cfg) => honoOutDir(g, cfg),
|
|
456
|
+
options: (g, cfg) => honoOptions(g, cfg)
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
kind: "express",
|
|
460
|
+
specifier: "@drzl/generator-express",
|
|
461
|
+
load: () => import("@drzl/generator-express"),
|
|
462
|
+
construct: (m, analysis) => new m.ExpressGenerator(analysis),
|
|
463
|
+
outputDir: (g, cfg) => expressOutDir(g, cfg),
|
|
464
|
+
options: (g, cfg) => expressOptions(g, cfg)
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
kind: "fastify",
|
|
468
|
+
specifier: "@drzl/generator-fastify",
|
|
469
|
+
load: () => import("@drzl/generator-fastify"),
|
|
470
|
+
construct: (m, analysis) => new m.FastifyGenerator(analysis),
|
|
471
|
+
outputDir: (g, cfg) => fastifyOutDir(g, cfg),
|
|
472
|
+
options: (g, cfg) => fastifyOptions(g, cfg)
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
kind: "nestjs",
|
|
476
|
+
specifier: "@drzl/generator-nestjs",
|
|
477
|
+
load: () => import("@drzl/generator-nestjs"),
|
|
478
|
+
construct: (m, analysis) => new m.NestJSGenerator(analysis),
|
|
479
|
+
outputDir: (g, cfg) => nestjsOutDir(g, cfg),
|
|
480
|
+
options: (g, cfg) => nestjsOptions(g, cfg)
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
kind: "graphql",
|
|
484
|
+
specifier: "@drzl/generator-graphql",
|
|
485
|
+
load: () => import("@drzl/generator-graphql"),
|
|
486
|
+
construct: (m, analysis) => new m.GraphQLGenerator(analysis),
|
|
487
|
+
outputDir: (g, cfg) => graphqlOutDir(g, cfg),
|
|
488
|
+
options: (g, cfg) => graphqlOptions(g, cfg)
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
kind: "service",
|
|
492
|
+
specifier: "@drzl/generator-service",
|
|
493
|
+
load: () => import("@drzl/generator-service"),
|
|
494
|
+
construct: (m, analysis) => new m.ServiceGenerator(analysis),
|
|
495
|
+
outputDir: (g) => g.path ?? SERVICES_DEFAULT_DIR,
|
|
496
|
+
options: (g, _cfg, ctx) => serviceOptions(g, ctx.outDir)
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
kind: "zod",
|
|
500
|
+
specifier: "@drzl/generator-zod",
|
|
501
|
+
load: () => import("@drzl/generator-zod"),
|
|
502
|
+
construct: (m, analysis) => new m.ZodGenerator(analysis),
|
|
503
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.zod,
|
|
504
|
+
// `meta` is zod-only; see `GeneratorCapabilities.meta` for why it is not passed to the other
|
|
505
|
+
// four rather than being passed and ignored.
|
|
506
|
+
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, {
|
|
507
|
+
schemaTypes: true,
|
|
508
|
+
meta: true,
|
|
509
|
+
constraints: true
|
|
510
|
+
})
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
kind: "valibot",
|
|
514
|
+
specifier: "@drzl/generator-valibot",
|
|
515
|
+
load: () => import("@drzl/generator-valibot"),
|
|
516
|
+
construct: (m, analysis) => new m.ValibotGenerator(analysis),
|
|
517
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.valibot,
|
|
518
|
+
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, constraints: true })
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
kind: "arktype",
|
|
522
|
+
specifier: "@drzl/generator-arktype",
|
|
523
|
+
load: () => import("@drzl/generator-arktype"),
|
|
524
|
+
construct: (m, analysis) => new m.ArkTypeGenerator(analysis),
|
|
525
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.arktype,
|
|
526
|
+
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: false })
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
kind: "typebox",
|
|
530
|
+
specifier: "@drzl/generator-typebox",
|
|
531
|
+
load: () => import("@drzl/generator-typebox"),
|
|
532
|
+
construct: (m, analysis) => new m.TypeBoxGenerator(analysis),
|
|
533
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.typebox,
|
|
534
|
+
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true, standardSchema: true })
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
kind: "effect",
|
|
538
|
+
specifier: "@drzl/generator-effect",
|
|
539
|
+
load: () => import("@drzl/generator-effect"),
|
|
540
|
+
construct: (m, analysis) => new m.EffectGenerator(analysis),
|
|
541
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS.effect,
|
|
542
|
+
options: (g, cfg, ctx) => validationOptions(g, cfg, ctx.outDir, { schemaTypes: true })
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
kind: "json-schema",
|
|
546
|
+
specifier: "@drzl/generator-json-schema",
|
|
547
|
+
load: () => import("@drzl/generator-json-schema"),
|
|
548
|
+
construct: (m, analysis) => new m.JsonSchemaGenerator(analysis),
|
|
549
|
+
outputDir: (g) => g.path ?? VALIDATOR_DEFAULT_DIRS["json-schema"],
|
|
550
|
+
options: (g, cfg, ctx) => jsonSchemaOptions(g, cfg, ctx.outDir)
|
|
551
|
+
}
|
|
552
|
+
];
|
|
553
|
+
var GENERATOR_BY_KIND = new Map(
|
|
554
|
+
GENERATORS.map((entry) => [entry.kind, entry])
|
|
555
|
+
);
|
|
556
|
+
function entryFor(kind) {
|
|
557
|
+
const entry = GENERATOR_BY_KIND.get(kind);
|
|
558
|
+
if (!entry) throw new Error(`No generator is registered for kind "${kind}".`);
|
|
559
|
+
return entry;
|
|
560
|
+
}
|
|
561
|
+
function filesOf(result) {
|
|
562
|
+
return Array.isArray(result) ? result : result.files;
|
|
563
|
+
}
|
|
564
|
+
async function runGenerator(entry, g, cfg, ctx) {
|
|
565
|
+
return runGeneratorWithOptions(entry, ctx.analysis, {
|
|
566
|
+
...entry.options(g, cfg, {
|
|
567
|
+
outDir: entry.outputDir(g, cfg),
|
|
568
|
+
servicesDir: ctx.servicesDir
|
|
569
|
+
}),
|
|
570
|
+
...ctx.fileSink ? { fileSink: ctx.fileSink } : {},
|
|
571
|
+
...ctx.onProgress ? { onProgress: ctx.onProgress } : {}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
async function runGeneratorWithOptions(entry, analysis, options) {
|
|
575
|
+
const module = await loadGenerator(entry.specifier, entry.load);
|
|
576
|
+
return filesOf(await entry.construct(module, analysis).generate(options));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/kind-selection.ts
|
|
580
|
+
var PIPELINE_PREFIX = "generate-";
|
|
581
|
+
var KindSelectionError = class extends Error {
|
|
582
|
+
constructor(code, message, hint) {
|
|
583
|
+
super(message);
|
|
584
|
+
this.code = code;
|
|
585
|
+
this.hint = hint;
|
|
586
|
+
this.name = "KindSelectionError";
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
function kindList() {
|
|
590
|
+
return GENERATOR_KINDS.join(", ");
|
|
591
|
+
}
|
|
592
|
+
function isKind(value) {
|
|
593
|
+
return GENERATOR_KINDS.includes(value);
|
|
594
|
+
}
|
|
595
|
+
function parseOnly(value, flag = "--only") {
|
|
596
|
+
if (value === void 0 || value === null) return void 0;
|
|
597
|
+
const requested = String(value).split(",").map((part) => part.trim()).filter(Boolean);
|
|
598
|
+
if (!requested.length) {
|
|
599
|
+
throw new KindSelectionError(
|
|
600
|
+
"DRZL_CLI_ONLY",
|
|
601
|
+
`${flag} was given no kind. Pass one or more of: ${kindList()}.`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
605
|
+
for (const name of requested) {
|
|
606
|
+
if (isKind(name)) {
|
|
607
|
+
kinds.add(name);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const bare = name.startsWith(PIPELINE_PREFIX) ? name.slice(PIPELINE_PREFIX.length) : "";
|
|
611
|
+
throw new KindSelectionError(
|
|
612
|
+
"DRZL_CLI_ONLY",
|
|
613
|
+
`${flag}: there is no generator kind "${name}".`,
|
|
614
|
+
isKind(bare) ? `Write it the way the config does: ${flag} ${bare}.` : `Valid kinds are: ${kindList()}.`
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
return kinds;
|
|
618
|
+
}
|
|
619
|
+
function resolveWatchSelection(opts) {
|
|
620
|
+
const only = parseOnly(opts.only);
|
|
621
|
+
const pipeline = opts.pipeline === void 0 || opts.pipeline === null ? "all" : String(opts.pipeline);
|
|
622
|
+
if (pipeline === "analyze") {
|
|
623
|
+
if (only) {
|
|
624
|
+
throw new KindSelectionError(
|
|
625
|
+
"DRZL_CLI_ONLY",
|
|
626
|
+
"--pipeline analyze runs no generator, so it cannot be combined with --only.",
|
|
627
|
+
"Drop one of the two."
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
return { analyzeOnly: true };
|
|
631
|
+
}
|
|
632
|
+
if (pipeline === "all") return { analyzeOnly: false, kinds: only };
|
|
633
|
+
if (only) {
|
|
634
|
+
throw new KindSelectionError(
|
|
635
|
+
"DRZL_CLI_ONLY",
|
|
636
|
+
"--pipeline and --only say the same thing, so passing both is ambiguous.",
|
|
637
|
+
`Use --only ${[...only].join(",")} on its own; --pipeline is the older spelling.`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const bare = pipeline.startsWith(PIPELINE_PREFIX) ? pipeline.slice(PIPELINE_PREFIX.length) : "";
|
|
641
|
+
if (!isKind(bare)) {
|
|
642
|
+
throw new KindSelectionError(
|
|
643
|
+
"DRZL_CLI_ONLY",
|
|
644
|
+
`--pipeline: there is no pipeline called "${pipeline}".`,
|
|
645
|
+
// A bare kind is the mirror image of the mistake `parseOnly` names, and the answer is the
|
|
646
|
+
// flag that takes bare kinds rather than the list of sixteen values this one takes.
|
|
647
|
+
isKind(pipeline) ? `That is a generator kind, so it goes to the newer flag: --only ${pipeline}.` : `Use --only <kind>, or one of: all, analyze, ${GENERATOR_KINDS.map(
|
|
648
|
+
(k) => PIPELINE_PREFIX + k
|
|
649
|
+
).join(", ")}.`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
return { analyzeOnly: false, kinds: /* @__PURE__ */ new Set([bare]) };
|
|
653
|
+
}
|
|
654
|
+
function selectGenerators(generators, kinds) {
|
|
655
|
+
if (!kinds) return [...generators];
|
|
656
|
+
return generators.filter((g) => kinds.has(g.kind));
|
|
657
|
+
}
|
|
658
|
+
function emptySelectionMessage(kinds, configured, flag = "--only") {
|
|
659
|
+
if (!kinds || selectGenerators(configured, kinds).length) return void 0;
|
|
660
|
+
const asked = [...kinds].join(", ");
|
|
661
|
+
const names = [...new Set(configured.map((g) => g.kind))];
|
|
662
|
+
return `${flag} ${asked} matched no generator in this config, which names: ${names.join(", ") || "none"}.`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/schema-outcome.ts
|
|
666
|
+
var SCHEMA_UNREADABLE_CODE = "DRZL_SCHEMA_001";
|
|
667
|
+
var SCHEMA_EMPTY_CODE = "DRZL_SCHEMA_002";
|
|
668
|
+
var SCHEMA_FILTERED_CODE = "DRZL_SCHEMA_003";
|
|
669
|
+
var NOTHING_GENERATED = "Nothing was generated.";
|
|
670
|
+
var UNREADABLE_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_NOFILE", "DRZL_ANL_IMPORT"]);
|
|
671
|
+
function firstLine(message) {
|
|
672
|
+
return String(message).split("\n")[0].trim();
|
|
673
|
+
}
|
|
674
|
+
function describeSchemaTarget(schema) {
|
|
675
|
+
if (typeof schema === "string") return schema;
|
|
676
|
+
if (schema.length === 1) return schema[0];
|
|
677
|
+
return `${schema.length} schema files`;
|
|
678
|
+
}
|
|
679
|
+
function schemaLoadFailure(issues, schema, consequence = NOTHING_GENERATED) {
|
|
680
|
+
const blocking = issues.filter(
|
|
681
|
+
(issue) => issue.level === "error" && issue.code && UNREADABLE_CODES.has(issue.code)
|
|
682
|
+
);
|
|
683
|
+
if (!blocking.length) return void 0;
|
|
684
|
+
const first = blocking[0];
|
|
685
|
+
const more = blocking.length > 1 ? ` (and ${blocking.length - 1} more)` : "";
|
|
686
|
+
const single = typeof schema === "string" ? schema : schema.length === 1 ? schema[0] : void 0;
|
|
687
|
+
if (first.code === "DRZL_ANL_NOFILE") {
|
|
688
|
+
const named = single ?? afterPrefix(first.message, "Schema file not found:");
|
|
689
|
+
return {
|
|
690
|
+
code: SCHEMA_UNREADABLE_CODE,
|
|
691
|
+
message: `Schema file not found (${SCHEMA_UNREADABLE_CODE}): ${named}${more}`,
|
|
692
|
+
hint: 'Check the "schema" path in your drzl config, or point --config at another one. ' + consequence
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const reason = single ? firstLine(afterPrefix(first.message, "Failed to import schema:")) : firstLine(String(first.message ?? ""));
|
|
696
|
+
const message = single ? `Could not load the schema module ${single} (${SCHEMA_UNREADABLE_CODE}): ${reason}${more}` : `Could not load a schema module (${SCHEMA_UNREADABLE_CODE}): ${reason}${more}`;
|
|
697
|
+
return {
|
|
698
|
+
code: SCHEMA_UNREADABLE_CODE,
|
|
699
|
+
message,
|
|
700
|
+
hint: single ? `Fix that error and run again. \`drzl analyze ${single}\` prints it in full. ${consequence}` : `Fix that error and run again. ${consequence}`
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function afterPrefix(message, prefix) {
|
|
704
|
+
const text = String(message ?? "");
|
|
705
|
+
return text.startsWith(prefix) ? text.slice(prefix.length).trim() : text;
|
|
706
|
+
}
|
|
707
|
+
function nothingToGenerate(opts) {
|
|
708
|
+
if (opts.remaining.length > 0) return void 0;
|
|
709
|
+
const target = describeSchemaTarget(opts.schema);
|
|
710
|
+
const consequence = opts.consequence ?? NOTHING_GENERATED;
|
|
711
|
+
if (!opts.analyzed.length) {
|
|
712
|
+
return {
|
|
713
|
+
code: SCHEMA_EMPTY_CODE,
|
|
714
|
+
message: `No Drizzle tables found in ${target} (${SCHEMA_EMPTY_CODE}).`,
|
|
715
|
+
hint: "That module imported cleanly and exported no tables, so every generator would write an empty barrel. Export them from it, for example: export const users = pgTable(...). " + consequence
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
const names = opts.analyzed.map((table) => table.name);
|
|
719
|
+
const shown = names.slice(0, 6).join(", ");
|
|
720
|
+
const rest = names.length > 6 ? `, and ${names.length - 6} more` : "";
|
|
721
|
+
return {
|
|
722
|
+
code: SCHEMA_FILTERED_CODE,
|
|
723
|
+
message: `Every table was removed by this config's filters (${SCHEMA_FILTERED_CODE}). ${target} declares ${names.length} table${names.length === 1 ? "" : "s"}: ${shown}${rest}.`,
|
|
724
|
+
hint: 'Check "include" and "exclude" in your drzl config. A pattern is matched against the whole database table name, with * as the only metacharacter. ' + consequence
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
93
728
|
// src/column-filter.ts
|
|
94
729
|
import { parseCheck as parseCheck2 } from "@drzl/validation-core";
|
|
95
730
|
|
|
96
731
|
// src/doctor.ts
|
|
97
|
-
import { parseCheck } from "@drzl/validation-core";
|
|
98
|
-
import
|
|
732
|
+
import { lengthMeasure, parseCheck } from "@drzl/validation-core";
|
|
733
|
+
import { Chalk as Chalk2 } from "chalk";
|
|
734
|
+
var PLAIN = new Chalk2({ level: 0 });
|
|
99
735
|
var HANDLED_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_UNKNOWN_COLUMN"]);
|
|
100
|
-
function splitPath(
|
|
101
|
-
if (!
|
|
102
|
-
const dot =
|
|
103
|
-
if (dot <= 0) return { table:
|
|
104
|
-
return { table:
|
|
736
|
+
function splitPath(path8) {
|
|
737
|
+
if (!path8) return {};
|
|
738
|
+
const dot = path8.lastIndexOf(".");
|
|
739
|
+
if (dot <= 0) return { table: path8 };
|
|
740
|
+
return { table: path8.slice(0, dot), column: path8.slice(dot + 1) };
|
|
105
741
|
}
|
|
106
742
|
function namedColumns(parsed) {
|
|
107
743
|
const out = [];
|
|
@@ -109,6 +745,7 @@ function namedColumns(parsed) {
|
|
|
109
745
|
for (const s of parsed.sets ?? []) out.push({ column: s.column, scalar: true });
|
|
110
746
|
for (const l of parsed.lengths ?? []) out.push({ column: l.column, scalar: false });
|
|
111
747
|
for (const c of parsed.cardinalities ?? []) out.push({ column: c.column, scalar: false });
|
|
748
|
+
for (const n of parsed.nulls ?? []) out.push({ column: n.column, scalar: false });
|
|
112
749
|
for (const r of parsed.rows ?? []) {
|
|
113
750
|
out.push({ column: r.left, scalar: false });
|
|
114
751
|
out.push({ column: r.right, scalar: false });
|
|
@@ -137,6 +774,19 @@ function describeShape(c) {
|
|
|
137
774
|
return "a structured";
|
|
138
775
|
}
|
|
139
776
|
}
|
|
777
|
+
var countNoun = (l) => l.unit === "bytes" ? "byte count" : "character count";
|
|
778
|
+
function countHint(c) {
|
|
779
|
+
if (c.shape?.kind === "byteString")
|
|
780
|
+
return "A binary(n)/varbinary(n) column hands the caller a string produced by a lossy decode, so its width is code points coming out and bytes going in and neither is a count of the value in hand. The column already caps itself at n bytes; a second bound stated here would be a different measurement. Leave this one to the database.";
|
|
781
|
+
return "Only constraints whose meaning is unambiguous are translated, because a validator enforcing a guess rejects rows the database accepts. Your database still enforces this one; nothing DRZL emits does.";
|
|
782
|
+
}
|
|
783
|
+
function declineHint(reason) {
|
|
784
|
+
if (/combined with/.test(reason))
|
|
785
|
+
return "Postgres computes numeric arithmetic exactly and JavaScript computes it in binary floating point, so `x + y <= 0.3` accepts (0.1, 0.2) in the database and rejects it in JavaScript. The right translation depends on whether the columns are numeric, double precision or bigint, and the expression does not say. Put the result in a generated column and constrain that, or leave this one to the database.";
|
|
786
|
+
if (/\bOR\b/.test(reason))
|
|
787
|
+
return "A disjunction is read only where the whole of it pins one column to a set of values, such as `status = 'a' OR status = 'b'`, which becomes the same enum an IN list does. Anything else is refused whole rather than in part: a row satisfying the other branch is one the database accepts, and enforcing one branch would turn it away.";
|
|
788
|
+
return "Only constraints whose meaning is unambiguous are translated, because a validator enforcing a guess rejects rows the database accepts. Your database still enforces this one; nothing DRZL emits does.";
|
|
789
|
+
}
|
|
140
790
|
function checkFindings(table) {
|
|
141
791
|
const out = [];
|
|
142
792
|
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
@@ -152,10 +802,34 @@ function checkFindings(table) {
|
|
|
152
802
|
table: table.tsName,
|
|
153
803
|
constraint: k.name,
|
|
154
804
|
message: `CHECK ${label} on "${table.tsName}" is not translated: ${parsed.reason}. Expression: ${expr}`,
|
|
155
|
-
hint:
|
|
805
|
+
hint: declineHint(parsed.reason)
|
|
156
806
|
});
|
|
157
807
|
continue;
|
|
158
808
|
}
|
|
809
|
+
for (const n of parsed.nulls ?? []) {
|
|
810
|
+
if (n.notNull) continue;
|
|
811
|
+
out.push({
|
|
812
|
+
kind: "check-declined",
|
|
813
|
+
level: "warn",
|
|
814
|
+
table: table.tsName,
|
|
815
|
+
constraint: k.name,
|
|
816
|
+
message: `CHECK ${label} on "${table.tsName}" holds "${n.column} IS NULL", which narrows the column to NULL alone and no generated schema states. Expression: ${expr}`,
|
|
817
|
+
hint: "A column that may only ever be NULL is usually a constraint written the wrong way round. Drop the column, or state the rule as a CHECK on the column that decides it."
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
for (const l of parsed.lengths ?? []) {
|
|
821
|
+
const col = byName.get(l.column);
|
|
822
|
+
if (!col || lengthMeasure(col, l)) continue;
|
|
823
|
+
out.push({
|
|
824
|
+
kind: "check-uncountable",
|
|
825
|
+
level: "warn",
|
|
826
|
+
table: table.tsName,
|
|
827
|
+
column: l.column,
|
|
828
|
+
constraint: k.name,
|
|
829
|
+
message: `CHECK ${label} on "${table.tsName}" counts ${describeShape(col)} column "${l.column}", whose ${countNoun(l)} in JavaScript is not the one the database took, so it is not translated. Expression: ${expr}`,
|
|
830
|
+
hint: countHint(col)
|
|
831
|
+
});
|
|
832
|
+
}
|
|
159
833
|
const seen = /* @__PURE__ */ new Set();
|
|
160
834
|
for (const { column, scalar } of namedColumns(parsed)) {
|
|
161
835
|
if (seen.has(column)) continue;
|
|
@@ -267,7 +941,7 @@ var SECTIONS = [
|
|
|
267
941
|
why: "These get a validator that accepts any value."
|
|
268
942
|
},
|
|
269
943
|
{
|
|
270
|
-
kinds: ["check-declined", "check-unknown-column", "check-not-scalar"],
|
|
944
|
+
kinds: ["check-declined", "check-unknown-column", "check-not-scalar", "check-uncountable"],
|
|
271
945
|
title: "CHECK constraints DRZL does not enforce",
|
|
272
946
|
why: "Your database still enforces these. Nothing DRZL generates does."
|
|
273
947
|
},
|
|
@@ -296,7 +970,8 @@ function wrap(text, indent, first = indent, width = 96) {
|
|
|
296
970
|
if (line) lines.push(line);
|
|
297
971
|
return lines.map((l, i) => (i === 0 ? first : indent) + l).join("\n");
|
|
298
972
|
}
|
|
299
|
-
function renderDoctorReport(report) {
|
|
973
|
+
function renderDoctorReport(report, style = PLAIN) {
|
|
974
|
+
const chalk = style;
|
|
300
975
|
const out = [];
|
|
301
976
|
const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
302
977
|
out.push(chalk.bold(`DRZL doctor ${report.schema}`));
|
|
@@ -444,87 +1119,1427 @@ function filterColumns(tables, spec) {
|
|
|
444
1119
|
return { tables: out, warnings };
|
|
445
1120
|
}
|
|
446
1121
|
|
|
447
|
-
// src/
|
|
448
|
-
import {
|
|
449
|
-
import
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
entries = await fs.readdir(current, { withFileTypes: true });
|
|
456
|
-
} catch {
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
for (const e of entries) {
|
|
460
|
-
const full = path.join(current, e.name);
|
|
461
|
-
if (e.isDirectory()) await walk(full);
|
|
462
|
-
else out.set(path.relative(dir, full), await fs.readFile(full, "utf8"));
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
await walk(dir);
|
|
466
|
-
return out;
|
|
1122
|
+
// src/explain.ts
|
|
1123
|
+
import { qualifiedForeignTable, qualifiedTableName } from "@drzl/analyzer";
|
|
1124
|
+
import { tableConstraints } from "@drzl/validation-core";
|
|
1125
|
+
import { Chalk as Chalk3 } from "chalk";
|
|
1126
|
+
var PLAIN2 = new Chalk3({ level: 0 });
|
|
1127
|
+
function namesOf(table) {
|
|
1128
|
+
const [bare, qualified] = tableAliases(table);
|
|
1129
|
+
return { qualified, name: bare, tsName: table.tsName };
|
|
467
1130
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
1131
|
+
var MATCH_ORDER = ["qualified", "name", "tsName"];
|
|
1132
|
+
var MATCH_LABELS = {
|
|
1133
|
+
qualified: "the schema-qualified name",
|
|
1134
|
+
name: "the database name",
|
|
1135
|
+
tsName: "the export name"
|
|
1136
|
+
};
|
|
1137
|
+
function hitsFor(tables, query, fold) {
|
|
1138
|
+
const wanted = fold(query);
|
|
1139
|
+
const hits = [];
|
|
1140
|
+
for (const table of tables) {
|
|
1141
|
+
const names = namesOf(table);
|
|
1142
|
+
const matchedOn = MATCH_ORDER.find((key) => fold(names[key]) === wanted);
|
|
1143
|
+
if (matchedOn) hits.push({ table, matchedOn });
|
|
474
1144
|
}
|
|
475
|
-
return
|
|
1145
|
+
return hits;
|
|
476
1146
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1147
|
+
var same = (s) => s;
|
|
1148
|
+
var folded = (s) => s.toLowerCase();
|
|
1149
|
+
function matchTable(tables, query) {
|
|
1150
|
+
for (const [exact, fold] of [
|
|
1151
|
+
[true, same],
|
|
1152
|
+
[false, folded]
|
|
1153
|
+
]) {
|
|
1154
|
+
const hits = hitsFor(tables, query, fold);
|
|
1155
|
+
if (hits.length === 1) return { kind: "found", exact, ...hits[0] };
|
|
1156
|
+
if (hits.length > 1) return { kind: "ambiguous", exact, hits };
|
|
485
1157
|
}
|
|
486
|
-
|
|
1158
|
+
const known = tables.flatMap((t) => {
|
|
1159
|
+
const names = namesOf(t);
|
|
1160
|
+
return t.tsName === names.name ? [names.name] : [names.name, names.tsName];
|
|
1161
|
+
});
|
|
1162
|
+
return { kind: "none", suggestion: nearestKey(query, known) };
|
|
487
1163
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
1164
|
+
function renderLiteral(value) {
|
|
1165
|
+
if (typeof value === "string") return `'${value.replace(/'/g, "\\'")}'`;
|
|
1166
|
+
if (value === null) return "null";
|
|
1167
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
1168
|
+
if (value instanceof Date) return value.toISOString();
|
|
1169
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
1170
|
+
return String(value);
|
|
496
1171
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
1172
|
+
function defaultOf(column) {
|
|
1173
|
+
if (column.defaultValue !== void 0) return { kind: "literal", value: column.defaultValue };
|
|
1174
|
+
if (column.defaultExpression) return { kind: "expression", text: column.defaultExpression };
|
|
1175
|
+
return column.hasDefault ? { kind: "runtime" } : null;
|
|
1176
|
+
}
|
|
1177
|
+
function describeDefault(value) {
|
|
1178
|
+
if (!value) return "";
|
|
1179
|
+
if (value.kind === "literal") return `default ${renderLiteral(value.value)}`;
|
|
1180
|
+
if (value.kind === "expression") return `default ${value.text}`;
|
|
1181
|
+
return "has default";
|
|
1182
|
+
}
|
|
1183
|
+
function describeShape2(shape) {
|
|
1184
|
+
switch (shape.kind) {
|
|
1185
|
+
case "buffer":
|
|
1186
|
+
return "binary payload, carried as a Uint8Array";
|
|
1187
|
+
case "json":
|
|
1188
|
+
return "any JSON value, checked recursively";
|
|
1189
|
+
case "tuple":
|
|
1190
|
+
return `tuple of ${shape.length} numbers`;
|
|
1191
|
+
case "numberObject":
|
|
1192
|
+
return `object of numbers: ${shape.fields.join(", ")}`;
|
|
1193
|
+
case "numberVector":
|
|
1194
|
+
return shape.length ? `numeric vector of ${shape.length}` : "numeric vector";
|
|
1195
|
+
case "custom":
|
|
1196
|
+
return shape.sqlType ? `customType, declared ${shape.sqlType}, with no runtime shape to read` : "customType, with no runtime shape to read";
|
|
1197
|
+
case "bitstring":
|
|
1198
|
+
if (shape.length === void 0) return "string of 0 and 1";
|
|
1199
|
+
return shape.exact ? `string of ${shape.length} digits, each 0 or 1` : `string of at most ${shape.length} digits, each 0 or 1`;
|
|
1200
|
+
case "byteString":
|
|
1201
|
+
return shape.length ? `bytes, declared width ${shape.length}` : "bytes";
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function capReason(column, narrowedBySet) {
|
|
1205
|
+
if (column.shape)
|
|
1206
|
+
return `"${column.name}" is a structured column, whose value space is not stated as a width`;
|
|
1207
|
+
if (narrowedBySet)
|
|
1208
|
+
return `a CHECK narrows "${column.name}" to a set of literals, which states its value space instead`;
|
|
1209
|
+
if (column.enumValues?.length)
|
|
1210
|
+
return `"${column.name}" is an enum, and its members state its value space instead`;
|
|
1211
|
+
if (column.tsType !== "string")
|
|
1212
|
+
return `"${column.name}" does not arrive as a string, so there is nothing to measure`;
|
|
1213
|
+
if (column.format)
|
|
1214
|
+
return `the ${column.format} format replaces the width on "${column.name}" rather than adding to it`;
|
|
1215
|
+
return `the generated schemas state "${column.name}" some other way`;
|
|
1216
|
+
}
|
|
1217
|
+
function factsFor(column, opts) {
|
|
1218
|
+
const facts = [];
|
|
1219
|
+
const state = (text) => facts.push({ text, stated: true });
|
|
1220
|
+
if (column.arrayDimensions) {
|
|
1221
|
+
state(
|
|
1222
|
+
column.arrayDimensions === 1 ? "an array of the type above" : `an array of ${column.arrayDimensions} dimensions`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
if (column.shape) state(describeShape2(column.shape));
|
|
1226
|
+
if (column.enumValues?.length) {
|
|
1227
|
+
state(`one of ${column.enumValues.map((v) => `'${v}'`).join(", ")}`);
|
|
1228
|
+
}
|
|
1229
|
+
if (column.format) state(`text in the ${column.format} format the database parses`);
|
|
1230
|
+
if (column.min !== void 0 && column.max !== void 0) {
|
|
1231
|
+
state(`${column.min} to ${column.max}`);
|
|
1232
|
+
} else if (column.min !== void 0) state(`at least ${column.min}`);
|
|
1233
|
+
else if (column.max !== void 0) state(`at most ${column.max}`);
|
|
1234
|
+
if (column.integer === true) state("whole numbers only");
|
|
1235
|
+
if (column.integer === false) state("fractions allowed");
|
|
1236
|
+
if (column.allowsNaN !== void 0) {
|
|
1237
|
+
state(column.allowsNaN ? "NaN is stored and returned" : "NaN is refused");
|
|
1238
|
+
}
|
|
1239
|
+
if (column.allowsInfinity !== void 0) {
|
|
1240
|
+
state(column.allowsInfinity ? "Infinity is stored and returned" : "Infinity is refused");
|
|
1241
|
+
}
|
|
1242
|
+
for (const [value2, text] of [
|
|
1243
|
+
[column.maxLength, `at most ${column.maxLength} characters`],
|
|
1244
|
+
[column.maxBytes, `at most ${column.maxBytes} bytes`]
|
|
1245
|
+
]) {
|
|
1246
|
+
if (value2 === void 0) continue;
|
|
1247
|
+
facts.push(
|
|
1248
|
+
opts.capStated ? { text, stated: true } : { text, stated: false, reason: capReason(column, opts.narrowedBySet) }
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
const value = defaultOf(column);
|
|
1252
|
+
if (value?.kind === "literal") state(`defaults to ${renderLiteral(value.value)}`);
|
|
1253
|
+
else if (value?.kind === "expression") state(`defaults to ${value.text}, evaluated by the database`);
|
|
1254
|
+
else if (value?.kind === "runtime") {
|
|
1255
|
+
facts.push({
|
|
1256
|
+
text: "has a default",
|
|
1257
|
+
stated: false,
|
|
1258
|
+
reason: "the value is produced at insert time, by the database or by a Drizzle function, so the field is optional on insert and no schema states what it becomes"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
if (column.isGenerated) {
|
|
1262
|
+
state("generated by the database, so it is left out of insert and update schemas");
|
|
1263
|
+
}
|
|
1264
|
+
return facts;
|
|
1265
|
+
}
|
|
1266
|
+
function issueTouches(issue, table) {
|
|
1267
|
+
if (!issue.path) return false;
|
|
1268
|
+
const names = namesOf(table);
|
|
1269
|
+
const own = [names.qualified, names.name, names.tsName, table.name];
|
|
1270
|
+
if (own.includes(issue.path)) return true;
|
|
1271
|
+
const dot = issue.path.lastIndexOf(".");
|
|
1272
|
+
return dot > 0 && own.includes(issue.path.slice(0, dot));
|
|
1273
|
+
}
|
|
1274
|
+
function issueColumn(issue, table) {
|
|
1275
|
+
const path8 = issue.path ?? "";
|
|
1276
|
+
const names = namesOf(table);
|
|
1277
|
+
for (const prefix of [names.qualified, names.tsName, names.name, table.name]) {
|
|
1278
|
+
if (path8.startsWith(`${prefix}.`)) {
|
|
1279
|
+
const rest = path8.slice(prefix.length + 1);
|
|
1280
|
+
if (table.columns.some((c) => c.name === rest)) return rest;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return void 0;
|
|
1284
|
+
}
|
|
1285
|
+
var RELATION_CODES = /* @__PURE__ */ new Set(["DRZL_ANL_RELATIONS", "DRZL_ANL_REL_V2"]);
|
|
1286
|
+
function gapsFor(table, constraints, issues) {
|
|
1287
|
+
const gaps = [];
|
|
1288
|
+
for (const constraint of constraints) {
|
|
1289
|
+
for (const part of constraint.unenforced ?? []) {
|
|
1290
|
+
gaps.push({
|
|
1291
|
+
kind: "check",
|
|
1292
|
+
subject: constraint.name ?? constraint.id,
|
|
1293
|
+
// `part.part` already carries the constraint name where the declaration had one, because
|
|
1294
|
+
// that is the text an emitted schema would have attached. The renderer prefixes `subject`
|
|
1295
|
+
// only when it is not already there, so a named CHECK is not announced twice.
|
|
1296
|
+
message: `${part.part} is not enforced: ${part.reason}.`,
|
|
1297
|
+
hint: "Your database still enforces it. Nothing DRZL generates does."
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
for (const issue of issues) {
|
|
1302
|
+
if (issue.level === "info") continue;
|
|
1303
|
+
if (!issueTouches(issue, table)) continue;
|
|
1304
|
+
const subject = issueColumn(issue, table);
|
|
1305
|
+
gaps.push({
|
|
1306
|
+
kind: RELATION_CODES.has(issue.code) ? "relation" : subject ? "column" : "analyzer",
|
|
1307
|
+
...subject ? { subject } : {},
|
|
1308
|
+
message: issue.message,
|
|
1309
|
+
...issue.hint ? { hint: issue.hint } : {}
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
return gaps;
|
|
1313
|
+
}
|
|
1314
|
+
function explainTable(analysis, match, options = {}) {
|
|
1315
|
+
const table = match.table;
|
|
1316
|
+
const qualified = qualifiedTableName(table);
|
|
1317
|
+
const constraints = tableConstraints(table).constraints;
|
|
1318
|
+
const capped = new Set(
|
|
1319
|
+
constraints.filter((c) => c.kind === "maxLength" || c.kind === "maxBytes").flatMap((c) => c.columns)
|
|
1320
|
+
);
|
|
1321
|
+
const narrowedBySet = new Set(
|
|
1322
|
+
constraints.filter((c) => c.values).map((c) => c.values.column)
|
|
1323
|
+
);
|
|
1324
|
+
const primaryKeyColumns = new Set(table.primaryKey?.columns ?? []);
|
|
1325
|
+
const singleColumnUnique = new Set(
|
|
1326
|
+
(table.unique ?? []).filter((u) => u.columns.length === 1).map((u) => u.columns[0])
|
|
1327
|
+
);
|
|
1328
|
+
const columns = table.columns.map((column) => ({
|
|
1329
|
+
name: column.name,
|
|
1330
|
+
tsType: column.tsType,
|
|
1331
|
+
dbType: column.dbType,
|
|
1332
|
+
...column.sqlType ? { sqlType: column.sqlType } : {},
|
|
1333
|
+
nullable: column.nullable,
|
|
1334
|
+
hasDefault: column.hasDefault,
|
|
1335
|
+
default: defaultOf(column),
|
|
1336
|
+
isGenerated: column.isGenerated,
|
|
1337
|
+
inPrimaryKey: primaryKeyColumns.has(column.name),
|
|
1338
|
+
unique: singleColumnUnique.has(column.name),
|
|
1339
|
+
...column.references ? { references: column.references } : {},
|
|
1340
|
+
...column.enumValues ? { enumValues: column.enumValues } : {},
|
|
1341
|
+
...column.arrayDimensions ? { arrayDimensions: column.arrayDimensions } : {},
|
|
1342
|
+
...column.shape ? { shape: column.shape } : {},
|
|
1343
|
+
facts: factsFor(column, {
|
|
1344
|
+
capStated: capped.has(column.name),
|
|
1345
|
+
narrowedBySet: narrowedBySet.has(column.name)
|
|
1346
|
+
})
|
|
1347
|
+
}));
|
|
1348
|
+
const relations = analysis.relations.filter((r) => r.from === qualified || r.to === qualified || r.via === qualified).map((r) => ({ ...r, outgoing: r.from === qualified }));
|
|
1349
|
+
const keyColumns = table.columns.filter((c) => primaryKeyColumns.has(c.name));
|
|
1350
|
+
const primaryKey = table.primaryKey?.columns.length ? {
|
|
1351
|
+
...table.primaryKey.name ? { name: table.primaryKey.name } : {},
|
|
1352
|
+
columns: [...table.primaryKey.columns],
|
|
1353
|
+
generated: keyColumns.length > 0 && keyColumns.every((c) => c.isGenerated || c.hasDefault)
|
|
1354
|
+
} : null;
|
|
1355
|
+
const removed = options.keptColumns ? table.columns.map((c) => c.name).filter((name) => !options.keptColumns.includes(name)) : [];
|
|
1356
|
+
return {
|
|
1357
|
+
name: table.name,
|
|
1358
|
+
tsName: table.tsName,
|
|
1359
|
+
...table.schema ? { schema: table.schema } : {},
|
|
1360
|
+
qualified,
|
|
1361
|
+
addressable: addressableName(table),
|
|
1362
|
+
readOnly: !!table.readOnly,
|
|
1363
|
+
matchedOn: match.matchedOn,
|
|
1364
|
+
matchedExactly: match.exact,
|
|
1365
|
+
...options.keptTables && !options.keptTables.includes(qualified) ? { excludedByConfig: true } : {},
|
|
1366
|
+
...removed.length ? { columnsRemovedByConfig: removed } : {},
|
|
1367
|
+
columns,
|
|
1368
|
+
primaryKey,
|
|
1369
|
+
unique: (table.unique ?? []).map((u) => ({
|
|
1370
|
+
...u.name ? { name: u.name } : {},
|
|
1371
|
+
columns: [...u.columns]
|
|
1372
|
+
})),
|
|
1373
|
+
indexes: (table.indexes ?? []).map((i) => ({
|
|
1374
|
+
...i.name ? { name: i.name } : {},
|
|
1375
|
+
columns: [...i.columns]
|
|
1376
|
+
})),
|
|
1377
|
+
foreignKeys: (table.foreignKeys ?? []).map((fk) => ({
|
|
1378
|
+
...fk.name ? { name: fk.name } : {},
|
|
1379
|
+
columns: [...fk.columns],
|
|
1380
|
+
references: { table: qualifiedForeignTable(fk), columns: [...fk.foreignColumns] },
|
|
1381
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
1382
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1383
|
+
})),
|
|
1384
|
+
relations,
|
|
1385
|
+
constraints,
|
|
1386
|
+
gaps: gapsFor(table, constraints, analysis.issues)
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function summarize(analysis) {
|
|
1390
|
+
return analysis.tables.map((table) => ({
|
|
1391
|
+
name: table.name,
|
|
1392
|
+
tsName: table.tsName,
|
|
1393
|
+
...table.schema ? { schema: table.schema } : {},
|
|
1394
|
+
qualified: qualifiedTableName(table),
|
|
1395
|
+
columns: table.columns.length,
|
|
1396
|
+
checks: table.checks?.length ?? 0,
|
|
1397
|
+
gaps: gapsFor(table, tableConstraints(table).constraints, analysis.issues).length
|
|
1398
|
+
}));
|
|
1399
|
+
}
|
|
1400
|
+
var WIDTH = 80;
|
|
1401
|
+
var pad = (text, width) => text + " ".repeat(Math.max(0, width - text.length));
|
|
1402
|
+
var widest = (values) => values.reduce((n, v) => Math.max(n, v.length), 0);
|
|
1403
|
+
function wrap2(text, indent, first = indent) {
|
|
1404
|
+
const lines = [];
|
|
1405
|
+
let line = "";
|
|
1406
|
+
for (const word of String(text).split(/\s+/)) {
|
|
1407
|
+
if (line && `${line} ${word}`.length + indent.length > WIDTH) {
|
|
1408
|
+
lines.push(line);
|
|
1409
|
+
line = word;
|
|
1410
|
+
} else {
|
|
1411
|
+
line = line ? `${line} ${word}` : word;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (line) lines.push(line);
|
|
1415
|
+
return lines.map((l, i) => (i === 0 ? first : indent) + l).join("\n");
|
|
1416
|
+
}
|
|
1417
|
+
function renderTsType(column) {
|
|
1418
|
+
return column.tsType + "[]".repeat(column.arrayDimensions ?? 0);
|
|
1419
|
+
}
|
|
1420
|
+
function columnNotes(column) {
|
|
1421
|
+
const notes = [];
|
|
1422
|
+
if (column.inPrimaryKey) notes.push("pk");
|
|
1423
|
+
if (column.unique) notes.push("unique");
|
|
1424
|
+
if (column.references) {
|
|
1425
|
+
notes.push(`fk -> ${column.references.table}.${column.references.column}`);
|
|
1426
|
+
}
|
|
1427
|
+
if (column.isGenerated) notes.push("generated");
|
|
1428
|
+
const value = describeDefault(column.default);
|
|
1429
|
+
if (value && !column.isGenerated) notes.push(value);
|
|
1430
|
+
return notes.join(", ");
|
|
1431
|
+
}
|
|
1432
|
+
function constraintLines(constraint, style, labelWidth) {
|
|
1433
|
+
const label = constraint.name ?? "";
|
|
1434
|
+
const verdict = constraint.enforced ? style.green("enforced") : style.yellow("not enforced by any generated schema");
|
|
1435
|
+
const out = [` ${pad(label, labelWidth)} ${constraint.rule}`];
|
|
1436
|
+
out.push(` ${" ".repeat(labelWidth)} ${verdict}`);
|
|
1437
|
+
for (const part of constraint.unenforced ?? []) {
|
|
1438
|
+
out.push(style.dim(wrap2(part.reason, " ".repeat(labelWidth + 4))));
|
|
1439
|
+
}
|
|
1440
|
+
return out;
|
|
1441
|
+
}
|
|
1442
|
+
function renderExplanation(explanation, context, style = PLAIN2) {
|
|
1443
|
+
const out = [];
|
|
1444
|
+
const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
1445
|
+
out.push(style.bold(explanation.qualified) + style.dim(` ${context.schema}`));
|
|
1446
|
+
const identity = [
|
|
1447
|
+
context.dialect,
|
|
1448
|
+
`table "${explanation.name}"`,
|
|
1449
|
+
`export "${explanation.tsName}"`,
|
|
1450
|
+
plural(explanation.columns.length, "column")
|
|
1451
|
+
];
|
|
1452
|
+
if (explanation.readOnly) identity.push("read-only, so no insert or update schema is emitted");
|
|
1453
|
+
out.push(style.dim(" " + identity.join(", ")));
|
|
1454
|
+
if (!explanation.matchedExactly) {
|
|
1455
|
+
out.push(style.dim(` matched on ${MATCH_LABELS[explanation.matchedOn]}, ignoring case`));
|
|
1456
|
+
}
|
|
1457
|
+
if (explanation.excludedByConfig) {
|
|
1458
|
+
out.push("");
|
|
1459
|
+
out.push(style.yellow(" This config's include/exclude removes this table."));
|
|
1460
|
+
out.push(style.dim(" No generator sees it, so nothing below reaches any emitted file."));
|
|
1461
|
+
}
|
|
1462
|
+
if (explanation.columnsRemovedByConfig?.length) {
|
|
1463
|
+
out.push("");
|
|
1464
|
+
out.push(
|
|
1465
|
+
style.yellow(
|
|
1466
|
+
` This config's columns filter removes ${explanation.columnsRemovedByConfig.length} of these columns: ${explanation.columnsRemovedByConfig.join(", ")}.`
|
|
1467
|
+
)
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
out.push("");
|
|
1471
|
+
out.push(style.bold("Columns"));
|
|
1472
|
+
const tsTypes = explanation.columns.map(renderTsType);
|
|
1473
|
+
const nameWidth = widest(["COLUMN", ...explanation.columns.map((c) => c.name)]);
|
|
1474
|
+
const tsWidth = widest(["TS TYPE", ...tsTypes]);
|
|
1475
|
+
const sqlWidth = widest(["SQL TYPE", ...explanation.columns.map((c) => c.sqlType ?? c.dbType)]);
|
|
1476
|
+
out.push(
|
|
1477
|
+
style.dim(
|
|
1478
|
+
` ${pad("COLUMN", nameWidth)} ${pad("TS TYPE", tsWidth)} ${pad("SQL TYPE", sqlWidth)} NULL`
|
|
1479
|
+
)
|
|
1480
|
+
);
|
|
1481
|
+
explanation.columns.forEach((column, i) => {
|
|
1482
|
+
const sql = column.sqlType ?? column.dbType;
|
|
1483
|
+
const notes = columnNotes(column);
|
|
1484
|
+
const nullable = column.nullable ? "yes" : "no";
|
|
1485
|
+
out.push(
|
|
1486
|
+
` ${pad(column.name, nameWidth)} ${pad(tsTypes[i], tsWidth)} ${pad(sql, sqlWidth)} ` + // Padded only when something follows it: a trailing run of spaces on every second row is
|
|
1487
|
+
// invisible in a terminal and is the first thing a test diff shows.
|
|
1488
|
+
(notes ? `${pad(nullable, 4)} ${style.dim(notes)}` : nullable)
|
|
1489
|
+
);
|
|
1490
|
+
});
|
|
1491
|
+
const withFacts = explanation.columns.filter((c) => c.facts.length);
|
|
1492
|
+
if (withFacts.length) {
|
|
1493
|
+
out.push("");
|
|
1494
|
+
out.push(style.bold("What the generators read off each column"));
|
|
1495
|
+
const factWidth = widest(withFacts.map((c) => c.name));
|
|
1496
|
+
for (const column of withFacts) {
|
|
1497
|
+
let first = true;
|
|
1498
|
+
for (const fact of column.facts) {
|
|
1499
|
+
const label = first ? pad(column.name, factWidth) : " ".repeat(factWidth);
|
|
1500
|
+
first = false;
|
|
1501
|
+
out.push(` ${label} ${fact.stated ? fact.text : style.yellow(fact.text)}`);
|
|
1502
|
+
if (fact.stated) continue;
|
|
1503
|
+
out.push(
|
|
1504
|
+
style.dim(
|
|
1505
|
+
wrap2(
|
|
1506
|
+
`not stated by any generated schema: ${fact.reason}`,
|
|
1507
|
+
" ".repeat(factWidth + 4)
|
|
1508
|
+
)
|
|
1509
|
+
)
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
out.push("");
|
|
1515
|
+
out.push(style.bold("Keys"));
|
|
1516
|
+
if (explanation.primaryKey) {
|
|
1517
|
+
const pk = explanation.primaryKey;
|
|
1518
|
+
out.push(
|
|
1519
|
+
` PRIMARY KEY (${pk.columns.join(", ")})` + (pk.generated ? style.dim(" filled in by the database") : "")
|
|
1520
|
+
);
|
|
1521
|
+
if (pk.columns.length > 1) {
|
|
1522
|
+
out.push(
|
|
1523
|
+
style.dim(
|
|
1524
|
+
wrap2(
|
|
1525
|
+
`The service and router generators key getById, update and delete on "${pk.columns[0]}" alone, so those operations match on part of this key.`,
|
|
1526
|
+
" "
|
|
1527
|
+
)
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
} else {
|
|
1532
|
+
out.push(style.yellow(" No primary key."));
|
|
1533
|
+
out.push(
|
|
1534
|
+
style.dim(
|
|
1535
|
+
wrap2(
|
|
1536
|
+
'The service and router generators fall back to a column named "id".',
|
|
1537
|
+
" "
|
|
1538
|
+
)
|
|
1539
|
+
)
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
for (const unique of explanation.unique) {
|
|
1543
|
+
out.push(` UNIQUE (${unique.columns.join(", ")})` + (unique.name ? style.dim(` ${unique.name}`) : ""));
|
|
1544
|
+
}
|
|
1545
|
+
for (const index of explanation.indexes) {
|
|
1546
|
+
out.push(style.dim(` INDEX (${index.columns.join(", ")})${index.name ? ` ${index.name}` : ""}`));
|
|
1547
|
+
}
|
|
1548
|
+
if (explanation.foreignKeys.length) {
|
|
1549
|
+
out.push("");
|
|
1550
|
+
out.push(style.bold("Foreign keys"));
|
|
1551
|
+
for (const fk of explanation.foreignKeys) {
|
|
1552
|
+
const actions = [
|
|
1553
|
+
fk.onDelete ? `ON DELETE ${fk.onDelete}` : "",
|
|
1554
|
+
fk.onUpdate ? `ON UPDATE ${fk.onUpdate}` : ""
|
|
1555
|
+
].filter(Boolean).join(" ");
|
|
1556
|
+
out.push(
|
|
1557
|
+
` (${fk.columns.join(", ")}) -> ${fk.references.table} (${fk.references.columns.join(", ")})` + (actions ? style.dim(` ${actions}`) : "")
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (explanation.relations.length) {
|
|
1562
|
+
out.push("");
|
|
1563
|
+
out.push(style.bold("Relations"));
|
|
1564
|
+
for (const relation of explanation.relations) {
|
|
1565
|
+
const via = relation.via ? ` through ${relation.via}` : "";
|
|
1566
|
+
out.push(
|
|
1567
|
+
` ${relation.from} -> ${relation.to}${via}` + style.dim(` ${relation.kind}`)
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const checks = explanation.constraints.filter((c) => c.kind === "check");
|
|
1572
|
+
if (checks.length) {
|
|
1573
|
+
out.push("");
|
|
1574
|
+
out.push(style.bold("CHECK constraints, as DRZL parsed them"));
|
|
1575
|
+
const labelWidth = widest(checks.map((c) => c.name ?? ""));
|
|
1576
|
+
for (const check of checks) out.push(...constraintLines(check, style, labelWidth));
|
|
1577
|
+
}
|
|
1578
|
+
out.push("");
|
|
1579
|
+
if (!explanation.gaps.length) {
|
|
1580
|
+
out.push(style.green("Nothing about this table was dropped or left unrecognised."));
|
|
1581
|
+
return out.join("\n");
|
|
1582
|
+
}
|
|
1583
|
+
out.push(style.yellow(`Not understood (${explanation.gaps.length})`));
|
|
1584
|
+
out.push(style.dim(" These are in your schema and are not in anything DRZL generates."));
|
|
1585
|
+
out.push("");
|
|
1586
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1587
|
+
for (const gap of explanation.gaps) {
|
|
1588
|
+
const key = gap.hint ?? "";
|
|
1589
|
+
groups.set(key, [...groups.get(key) ?? [], gap]);
|
|
1590
|
+
}
|
|
1591
|
+
for (const [hint, items] of groups) {
|
|
1592
|
+
for (const gap of items) {
|
|
1593
|
+
const named = gap.subject && !gap.message.startsWith(gap.subject);
|
|
1594
|
+
out.push(wrap2((named ? `${gap.subject}: ` : "") + gap.message, " ", ` ${style.dim("-")} `));
|
|
1595
|
+
}
|
|
1596
|
+
if (hint) out.push(style.dim(wrap2(hint, " ")));
|
|
1597
|
+
out.push("");
|
|
1598
|
+
}
|
|
1599
|
+
return out.join("\n").replace(/\n+$/, "");
|
|
1600
|
+
}
|
|
1601
|
+
function renderIndex(tables, context, style = PLAIN2) {
|
|
1602
|
+
const out = [];
|
|
1603
|
+
const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
1604
|
+
out.push(style.bold(context.schema) + style.dim(` ${context.dialect}`));
|
|
1605
|
+
out.push(style.dim(` ${plural(tables.length, "table")}`));
|
|
1606
|
+
out.push("");
|
|
1607
|
+
const nameWidth = widest(["TABLE", ...tables.map((t) => t.qualified)]);
|
|
1608
|
+
const tsWidth = widest(["EXPORT", ...tables.map((t) => t.tsName)]);
|
|
1609
|
+
out.push(style.dim(` ${pad("TABLE", nameWidth)} ${pad("EXPORT", tsWidth)} COLUMNS`));
|
|
1610
|
+
for (const table of tables) {
|
|
1611
|
+
const columns = String(table.columns);
|
|
1612
|
+
out.push(
|
|
1613
|
+
` ${pad(table.qualified, nameWidth)} ${pad(table.tsName, tsWidth)} ` + (table.gaps ? `${pad(columns, 7)} ` + style.yellow(`${plural(table.gaps, "thing")} not understood`) : columns)
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
out.push("");
|
|
1617
|
+
out.push(style.dim(" drzl explain <table> for one of them in full"));
|
|
1618
|
+
return out.join("\n");
|
|
1619
|
+
}
|
|
1620
|
+
var NO_SUCH_TABLE_CODE = "DRZL_EXPLAIN_001";
|
|
1621
|
+
var AMBIGUOUS_TABLE_CODE = "DRZL_EXPLAIN_002";
|
|
1622
|
+
var NAME_CAP = 12;
|
|
1623
|
+
function noSuchTableProblem(query, tables, suggestion) {
|
|
1624
|
+
const names = tables.map((t) => displayTableName(t));
|
|
1625
|
+
const shown = names.slice(0, NAME_CAP).join(", ");
|
|
1626
|
+
const rest = names.length > NAME_CAP ? `, and ${names.length - NAME_CAP} more` : "";
|
|
1627
|
+
return {
|
|
1628
|
+
code: NO_SUCH_TABLE_CODE,
|
|
1629
|
+
message: `No table called "${query}" (${NO_SUCH_TABLE_CODE}). ` + (names.length ? `This schema declares ${names.length} table${names.length === 1 ? "" : "s"}: ${shown}${rest}.` : "This schema declares no tables."),
|
|
1630
|
+
hint: suggestion ? `Did you mean "${suggestion}"?` : "A table is matched by its database name, by its schema-qualified name, or by the name it is exported under, ignoring case where nothing matches exactly."
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function ambiguousTableProblem(query, hits) {
|
|
1634
|
+
const named = hits.map((hit) => `${addressableName(hit.table)} (exported as ${hit.table.tsName})`).join(", ");
|
|
1635
|
+
return {
|
|
1636
|
+
code: AMBIGUOUS_TABLE_CODE,
|
|
1637
|
+
message: `"${query}" names ${hits.length} tables (${AMBIGUOUS_TABLE_CODE}): ${named}.`,
|
|
1638
|
+
hint: `Name one of them exactly, for example "${addressableName(hits[0].table)}".`
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/drizzle-kit.ts
|
|
1643
|
+
import * as fs from "fs";
|
|
1644
|
+
import * as path from "path";
|
|
1645
|
+
var DRIZZLE_KIT_CONFIG_CANDIDATES = [
|
|
1646
|
+
"drizzle.config.ts",
|
|
1647
|
+
"drizzle.config.js",
|
|
1648
|
+
"drizzle.config.json"
|
|
1649
|
+
];
|
|
1650
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".cjs", ".mjs", ".mts", ".cts"]);
|
|
1651
|
+
function findDrizzleKitConfig(cwd) {
|
|
1652
|
+
for (const name of DRIZZLE_KIT_CONFIG_CANDIDATES) {
|
|
1653
|
+
const p = path.join(cwd, name);
|
|
1654
|
+
if (fs.existsSync(p)) return p;
|
|
1655
|
+
}
|
|
1656
|
+
return null;
|
|
1657
|
+
}
|
|
1658
|
+
async function loadDrizzleKitConfig(p) {
|
|
1659
|
+
let raw;
|
|
1660
|
+
try {
|
|
1661
|
+
raw = await importFreshConfigModule(p);
|
|
1662
|
+
} catch (e) {
|
|
1663
|
+
throw new Error(
|
|
1664
|
+
`drzl config: failed to load the drizzle-kit config at ${p}: ${e?.message ?? e}`
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
if (!raw || typeof raw !== "object") {
|
|
1668
|
+
throw new Error(`drzl config: ${p} did not export a drizzle-kit config object.`);
|
|
1669
|
+
}
|
|
1670
|
+
const record = raw;
|
|
1671
|
+
const schema = record.schema;
|
|
1672
|
+
if (schema !== void 0 && typeof schema !== "string" && !(Array.isArray(schema) && schema.every((s) => typeof s === "string"))) {
|
|
1673
|
+
throw new Error(
|
|
1674
|
+
`drzl config: "schema" in ${p} must be a string or an array of strings, matching drizzle-kit's own Config type.`
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
path: p,
|
|
1679
|
+
schema,
|
|
1680
|
+
dialect: typeof record.dialect === "string" ? record.dialect : void 0,
|
|
1681
|
+
casing: typeof record.casing === "string" ? record.casing : void 0
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
function hasGlobMagic(entry) {
|
|
1685
|
+
return /[*?{}[\]]/.test(entry) || /[!@+]\(/.test(entry);
|
|
1686
|
+
}
|
|
1687
|
+
function staticGlobBase(entry, cwd) {
|
|
1688
|
+
const segments = entry.split("/");
|
|
1689
|
+
const kept = [];
|
|
1690
|
+
for (const s of segments) {
|
|
1691
|
+
if (hasGlobMagic(s)) break;
|
|
1692
|
+
kept.push(s);
|
|
1693
|
+
}
|
|
1694
|
+
const joined = kept.join("/");
|
|
1695
|
+
const base = path.resolve(cwd, joined || ".");
|
|
1696
|
+
if (fs.existsSync(base) && fs.statSync(base).isDirectory()) return base;
|
|
1697
|
+
return path.dirname(base);
|
|
1698
|
+
}
|
|
1699
|
+
function filesOneLevel(dir) {
|
|
1700
|
+
const out = [];
|
|
1701
|
+
for (const name of fs.readdirSync(dir)) {
|
|
1702
|
+
const full = path.join(dir, name);
|
|
1703
|
+
if (!fs.lstatSync(full).isDirectory()) out.push(full);
|
|
1704
|
+
}
|
|
1705
|
+
return out;
|
|
1706
|
+
}
|
|
1707
|
+
function expandSchemaPaths(entries, cwd) {
|
|
1708
|
+
const list = typeof entries === "string" ? [entries] : entries;
|
|
1709
|
+
const files = /* @__PURE__ */ new Set();
|
|
1710
|
+
const watchDirs = /* @__PURE__ */ new Set();
|
|
1711
|
+
for (const entry of list) {
|
|
1712
|
+
if (hasGlobMagic(entry)) {
|
|
1713
|
+
watchDirs.add(staticGlobBase(entry, cwd));
|
|
1714
|
+
for (const match of fs.globSync(entry, { cwd })) {
|
|
1715
|
+
const full2 = path.resolve(cwd, match);
|
|
1716
|
+
if (fs.existsSync(full2) && fs.statSync(full2).isDirectory()) {
|
|
1717
|
+
for (const f of filesOneLevel(full2)) files.add(f);
|
|
1718
|
+
} else {
|
|
1719
|
+
files.add(full2);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
const full = path.resolve(cwd, entry);
|
|
1725
|
+
let stat = null;
|
|
1726
|
+
try {
|
|
1727
|
+
stat = fs.statSync(full);
|
|
1728
|
+
} catch {
|
|
1729
|
+
watchDirs.add(path.dirname(full));
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
if (stat.isDirectory()) {
|
|
1733
|
+
watchDirs.add(full);
|
|
1734
|
+
for (const f of filesOneLevel(full)) files.add(f);
|
|
1735
|
+
} else {
|
|
1736
|
+
watchDirs.add(path.dirname(full));
|
|
1737
|
+
files.add(full);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
const kept = [...files].filter((f) => CODE_EXTENSIONS.has(path.extname(f).toLowerCase()));
|
|
1741
|
+
return { files: kept.sort(), watchDirs: [...watchDirs] };
|
|
1742
|
+
}
|
|
1743
|
+
function mapDrizzleKitDialect(declared) {
|
|
1744
|
+
if (!declared) return null;
|
|
1745
|
+
const map = {
|
|
1746
|
+
postgresql: "postgres",
|
|
1747
|
+
mysql: "mysql",
|
|
1748
|
+
sqlite: "sqlite",
|
|
1749
|
+
turso: "sqlite",
|
|
1750
|
+
singlestore: "singlestore",
|
|
1751
|
+
gel: "gel"
|
|
1752
|
+
};
|
|
1753
|
+
if (declared in map) return map[declared];
|
|
1754
|
+
const analyzerDialects = [
|
|
1755
|
+
"sqlite",
|
|
1756
|
+
"postgres",
|
|
1757
|
+
"mysql",
|
|
1758
|
+
"singlestore",
|
|
1759
|
+
"mssql",
|
|
1760
|
+
"cockroach",
|
|
1761
|
+
"gel"
|
|
1762
|
+
];
|
|
1763
|
+
return analyzerDialects.includes(declared) ? declared : null;
|
|
1764
|
+
}
|
|
1765
|
+
function dialectMismatchWarning(args) {
|
|
1766
|
+
const expected = mapDrizzleKitDialect(args.declared);
|
|
1767
|
+
if (!expected) return null;
|
|
1768
|
+
if (args.analyzed === "unknown") return null;
|
|
1769
|
+
if (args.analyzed === expected) return null;
|
|
1770
|
+
return `drzl: ${args.configPath} declares dialect "${args.declared}", but the schema analyzed as "${args.analyzed}". DRZL follows the schema; if the schema files are the right ones, the dialect in that config is stale.`;
|
|
1771
|
+
}
|
|
1772
|
+
async function resolveSchemaSource(cfg, cwd = process.cwd()) {
|
|
1773
|
+
if (cfg.schema) {
|
|
1774
|
+
const warnings = [];
|
|
1775
|
+
if (cfg.drizzleKit === true || typeof cfg.drizzleKit === "string") {
|
|
1776
|
+
warnings.push(
|
|
1777
|
+
`drzl config: both "schema" and "drizzleKit" are set. "schema" wins, so the drizzle-kit config was not read; remove one of the two to silence this.`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
return {
|
|
1781
|
+
source: "drzl",
|
|
1782
|
+
schema: cfg.schema,
|
|
1783
|
+
watchDirs: [path.dirname(path.resolve(cwd, cfg.schema))],
|
|
1784
|
+
warnings
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
if (cfg.drizzleKit === false) {
|
|
1788
|
+
throw new Error(
|
|
1789
|
+
`drzl config: no "schema" is set and "drizzleKit" is false, so the drizzle-kit fallback is disabled. Set "schema".`
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
let configPath;
|
|
1793
|
+
if (typeof cfg.drizzleKit === "string") {
|
|
1794
|
+
configPath = path.resolve(cwd, cfg.drizzleKit);
|
|
1795
|
+
if (!fs.existsSync(configPath)) {
|
|
1796
|
+
throw new Error(`drzl config: "drizzleKit" points at ${configPath}, which does not exist.`);
|
|
1797
|
+
}
|
|
1798
|
+
} else {
|
|
1799
|
+
const found = findDrizzleKitConfig(cwd);
|
|
1800
|
+
if (!found) {
|
|
1801
|
+
const looked = DRIZZLE_KIT_CONFIG_CANDIDATES.join(", ");
|
|
1802
|
+
throw new Error(
|
|
1803
|
+
cfg.drizzleKit === true ? `drzl config: "drizzleKit" is set, but no drizzle-kit config was found (looked for ${looked} in ${cwd}). Create one, or point "drizzleKit" at its path.` : `drzl config: no "schema" is set and no drizzle-kit config was found (looked for ${looked} in ${cwd}). Set "schema" in your drzl config, or add "drizzleKit" naming your drizzle-kit config file.`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
configPath = found;
|
|
1807
|
+
}
|
|
1808
|
+
const kit = await loadDrizzleKitConfig(configPath);
|
|
1809
|
+
if (kit.schema === void 0) {
|
|
1810
|
+
throw new Error(
|
|
1811
|
+
`drzl config: ${configPath} has no "schema" entry, so there is nothing to analyze. Set "schema" there, or set "schema" in your drzl config.`
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
const { files, watchDirs } = expandSchemaPaths(kit.schema, cwd);
|
|
1815
|
+
if (!files.length) {
|
|
1816
|
+
const shown = (typeof kit.schema === "string" ? [kit.schema] : kit.schema).map((s) => JSON.stringify(s)).join(", ");
|
|
1817
|
+
throw new Error(
|
|
1818
|
+
`drzl config: the "schema" patterns in ${configPath} matched no schema files: ${shown}. DRZL expands them the way drizzle-kit does; check them against your tree.`
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
return {
|
|
1822
|
+
source: "drizzle-kit",
|
|
1823
|
+
schema: files,
|
|
1824
|
+
watchDirs,
|
|
1825
|
+
drizzleKitConfigPath: configPath,
|
|
1826
|
+
drizzleKitDialect: kit.dialect,
|
|
1827
|
+
warnings: []
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// src/drift.ts
|
|
1832
|
+
import { promises as fs2 } from "fs";
|
|
1833
|
+
import path2 from "path";
|
|
1834
|
+
async function snapshotDir(dir) {
|
|
1835
|
+
const out = /* @__PURE__ */ new Map();
|
|
1836
|
+
async function walk(current) {
|
|
1837
|
+
let entries;
|
|
1838
|
+
try {
|
|
1839
|
+
entries = await fs2.readdir(current, { withFileTypes: true });
|
|
1840
|
+
} catch {
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
for (const e of entries) {
|
|
1844
|
+
const full = path2.join(current, e.name);
|
|
1845
|
+
if (e.isDirectory()) await walk(full);
|
|
1846
|
+
else out.set(path2.relative(dir, full), await fs2.readFile(full, "utf8"));
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
await walk(dir);
|
|
1850
|
+
return out;
|
|
1851
|
+
}
|
|
1852
|
+
async function snapshotAll(dirs) {
|
|
1853
|
+
const all = /* @__PURE__ */ new Map();
|
|
1854
|
+
for (const dir of dirs) {
|
|
1855
|
+
for (const [rel, content] of await snapshotDir(dir)) {
|
|
1856
|
+
all.set(path2.join(dir, rel), content);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
return all;
|
|
1860
|
+
}
|
|
1861
|
+
function diffSnapshots(before, after) {
|
|
1862
|
+
const out = [];
|
|
1863
|
+
for (const [file, content] of after) {
|
|
1864
|
+
if (!before.has(file)) out.push({ file, status: "added" });
|
|
1865
|
+
else if (before.get(file) !== content) out.push({ file, status: "changed" });
|
|
1866
|
+
}
|
|
1867
|
+
for (const file of before.keys()) {
|
|
1868
|
+
if (!after.has(file)) out.push({ file, status: "removed" });
|
|
1869
|
+
}
|
|
1870
|
+
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
1871
|
+
}
|
|
1872
|
+
async function restoreSnapshot(before, after) {
|
|
1873
|
+
for (const [file, content] of before) {
|
|
1874
|
+
await fs2.mkdir(path2.dirname(file), { recursive: true });
|
|
1875
|
+
await fs2.writeFile(file, content, "utf8");
|
|
1876
|
+
}
|
|
1877
|
+
for (const file of after.keys()) {
|
|
1878
|
+
if (!before.has(file)) await fs2.rm(file, { force: true });
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// src/emit-plan.ts
|
|
1883
|
+
import { promises as fs3 } from "fs";
|
|
1884
|
+
import path3 from "path";
|
|
1885
|
+
var EmitPlan = class {
|
|
1886
|
+
constructor(options) {
|
|
1887
|
+
this.byFile = /* @__PURE__ */ new Map();
|
|
1888
|
+
this.dirs = /* @__PURE__ */ new Set();
|
|
1889
|
+
this.writes = options.write;
|
|
1890
|
+
this.existing = options.existing;
|
|
1891
|
+
}
|
|
1892
|
+
async mkdir(dir) {
|
|
1893
|
+
this.dirs.add(dir);
|
|
1894
|
+
if (this.writes) await fs3.mkdir(dir, { recursive: true });
|
|
1895
|
+
}
|
|
1896
|
+
async writeFile(file, contents) {
|
|
1897
|
+
const prior = this.byFile.get(file);
|
|
1898
|
+
const before = prior ? prior.before : await this.read(file);
|
|
1899
|
+
this.byFile.set(file, {
|
|
1900
|
+
file,
|
|
1901
|
+
before,
|
|
1902
|
+
after: contents,
|
|
1903
|
+
verdict: before === null ? "created" : before === contents ? "unchanged" : "changed"
|
|
1904
|
+
});
|
|
1905
|
+
if (this.writes) await fs3.writeFile(file, contents, "utf8");
|
|
1906
|
+
}
|
|
1907
|
+
async read(file) {
|
|
1908
|
+
if (this.existing) return this.existing.get(file) ?? null;
|
|
1909
|
+
try {
|
|
1910
|
+
return await fs3.readFile(file, "utf8");
|
|
1911
|
+
} catch {
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
/** Every directory a generator asked for, whether or not it was created. */
|
|
1916
|
+
get directories() {
|
|
1917
|
+
return [...this.dirs];
|
|
1918
|
+
}
|
|
1919
|
+
/** Every recorded file, in the order it was first written. */
|
|
1920
|
+
get files() {
|
|
1921
|
+
return [...this.byFile.values()];
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* The verdicts for a list of paths, in the order given.
|
|
1925
|
+
*
|
|
1926
|
+
* A path with no verdict is a path the generator reported writing without routing it through
|
|
1927
|
+
* the sink, which is the one shape a version mismatch takes: a `@drzl/cli` that knows about
|
|
1928
|
+
* `fileSink` beside a generator package that predates it. It is returned rather than thrown on
|
|
1929
|
+
* so the caller can name the generator; see `unrecorded`.
|
|
1930
|
+
*/
|
|
1931
|
+
verdictsFor(paths) {
|
|
1932
|
+
return paths.map((p) => this.byFile.get(p));
|
|
1933
|
+
}
|
|
1934
|
+
/** The paths a generator claims to have written that never reached this sink. */
|
|
1935
|
+
unrecorded(paths) {
|
|
1936
|
+
return paths.filter((p) => !this.byFile.has(p));
|
|
1937
|
+
}
|
|
1938
|
+
counts(paths) {
|
|
1939
|
+
const entries = paths ? this.verdictsFor(paths).filter(Boolean) : this.files;
|
|
1940
|
+
const counts = { total: entries.length, created: 0, changed: 0, unchanged: 0 };
|
|
1941
|
+
for (const e of entries) counts[e.verdict]++;
|
|
1942
|
+
return counts;
|
|
505
1943
|
}
|
|
506
1944
|
};
|
|
507
|
-
function
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
1945
|
+
function describeCounts(counts) {
|
|
1946
|
+
const parts = [];
|
|
1947
|
+
if (counts.created) parts.push(`${counts.created} created`);
|
|
1948
|
+
if (counts.changed) parts.push(`${counts.changed} changed`);
|
|
1949
|
+
if (counts.unchanged) parts.push(`${counts.unchanged} unchanged`);
|
|
1950
|
+
return parts.join(", ") || "nothing to write";
|
|
1951
|
+
}
|
|
1952
|
+
function pendingChanges(plan) {
|
|
1953
|
+
return plan.files.filter((f) => f.verdict !== "unchanged").sort((a, b) => a.file.localeCompare(b.file));
|
|
1954
|
+
}
|
|
1955
|
+
function driftStatusOf(verdict) {
|
|
1956
|
+
return verdict === "created" ? "added" : "changed";
|
|
1957
|
+
}
|
|
1958
|
+
async function verifyNothingWasWritten(dirs, before) {
|
|
1959
|
+
const after = await snapshotAll(dirs);
|
|
1960
|
+
const drift = diffSnapshots(before, after);
|
|
1961
|
+
if (!drift.length) return [];
|
|
1962
|
+
await restoreSnapshot(before, after);
|
|
1963
|
+
return drift.map((d) => d.file).sort();
|
|
1964
|
+
}
|
|
1965
|
+
function displayPath(file, cwd = process.cwd()) {
|
|
1966
|
+
const rel = path3.relative(cwd, file);
|
|
1967
|
+
return rel && !rel.startsWith("..") ? rel : file;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/unified-diff.ts
|
|
1971
|
+
var DEFAULT_DIFF_LIMITS = {
|
|
1972
|
+
maxLines: 4e3,
|
|
1973
|
+
maxEdits: 1500,
|
|
1974
|
+
context: 3
|
|
1975
|
+
};
|
|
1976
|
+
function toLines(text) {
|
|
1977
|
+
if (text === "") return { lines: [], newlineAtEnd: true };
|
|
1978
|
+
const newlineAtEnd = text.endsWith("\n");
|
|
1979
|
+
const lines = text.split("\n");
|
|
1980
|
+
if (newlineAtEnd) lines.pop();
|
|
1981
|
+
return { lines, newlineAtEnd };
|
|
1982
|
+
}
|
|
1983
|
+
function shortestEdit(a, b, maxEdits) {
|
|
1984
|
+
const n = a.length;
|
|
1985
|
+
const m = b.length;
|
|
1986
|
+
const max = n + m;
|
|
1987
|
+
const offset = max;
|
|
1988
|
+
const v = new Int32Array(2 * max + 1);
|
|
1989
|
+
const trace = [];
|
|
1990
|
+
const limit = Math.min(max, maxEdits);
|
|
1991
|
+
for (let d = 0; d <= limit; d++) {
|
|
1992
|
+
trace.push(Int32Array.prototype.slice.call(v));
|
|
1993
|
+
for (let k = -d; k <= d; k += 2) {
|
|
1994
|
+
let x;
|
|
1995
|
+
if (k === -d || k !== d && v[k - 1 + offset] < v[k + 1 + offset]) x = v[k + 1 + offset];
|
|
1996
|
+
else x = v[k - 1 + offset] + 1;
|
|
1997
|
+
let y = x - k;
|
|
1998
|
+
while (x < n && y < m && a[x] === b[y]) {
|
|
1999
|
+
x++;
|
|
2000
|
+
y++;
|
|
2001
|
+
}
|
|
2002
|
+
v[k + offset] = x;
|
|
2003
|
+
if (x >= n && y >= m) return trace;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
function backtrack(trace, a, b) {
|
|
2009
|
+
const max = a.length + b.length;
|
|
2010
|
+
const offset = max;
|
|
2011
|
+
let x = a.length;
|
|
2012
|
+
let y = b.length;
|
|
2013
|
+
const ops = [];
|
|
2014
|
+
for (let d = trace.length - 1; d >= 0; d--) {
|
|
2015
|
+
const v = trace[d];
|
|
2016
|
+
const k = x - y;
|
|
2017
|
+
let prevK;
|
|
2018
|
+
if (k === -d || k !== d && v[k - 1 + offset] < v[k + 1 + offset]) prevK = k + 1;
|
|
2019
|
+
else prevK = k - 1;
|
|
2020
|
+
const prevX = v[prevK + offset];
|
|
2021
|
+
const prevY = prevX - prevK;
|
|
2022
|
+
while (x > prevX && y > prevY) {
|
|
2023
|
+
x--;
|
|
2024
|
+
y--;
|
|
2025
|
+
ops.push({ kind: "equal", a: x, b: y });
|
|
2026
|
+
}
|
|
2027
|
+
if (d > 0) {
|
|
2028
|
+
if (x === prevX) {
|
|
2029
|
+
y--;
|
|
2030
|
+
ops.push({ kind: "insert", a: x, b: y });
|
|
2031
|
+
} else {
|
|
2032
|
+
x--;
|
|
2033
|
+
ops.push({ kind: "delete", a: x, b: y });
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
ops.reverse();
|
|
2038
|
+
return ops;
|
|
2039
|
+
}
|
|
2040
|
+
function diffLines(a, b, maxEdits) {
|
|
2041
|
+
let head = 0;
|
|
2042
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head++;
|
|
2043
|
+
let tail = 0;
|
|
2044
|
+
while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) {
|
|
2045
|
+
tail++;
|
|
2046
|
+
}
|
|
2047
|
+
const midA = a.slice(head, a.length - tail);
|
|
2048
|
+
const midB = b.slice(head, b.length - tail);
|
|
2049
|
+
let mid = [];
|
|
2050
|
+
if (midA.length || midB.length) {
|
|
2051
|
+
const trace = shortestEdit(midA, midB, maxEdits);
|
|
2052
|
+
if (!trace) return null;
|
|
2053
|
+
mid = backtrack(trace, midA, midB);
|
|
2054
|
+
}
|
|
2055
|
+
const ops = [];
|
|
2056
|
+
for (let i = 0; i < head; i++) ops.push({ kind: "equal", a: i, b: i });
|
|
2057
|
+
for (const op of mid) ops.push({ kind: op.kind, a: op.a + head, b: op.b + head });
|
|
2058
|
+
for (let i = 0; i < tail; i++) {
|
|
2059
|
+
ops.push({ kind: "equal", a: a.length - tail + i, b: b.length - tail + i });
|
|
2060
|
+
}
|
|
2061
|
+
return ops;
|
|
2062
|
+
}
|
|
2063
|
+
function unifiedDiff(before, after, opts) {
|
|
2064
|
+
if (before === after) return "";
|
|
2065
|
+
const limits = { ...DEFAULT_DIFF_LIMITS, ...opts.limits ?? {} };
|
|
2066
|
+
const from = toLines(before);
|
|
2067
|
+
const to = toLines(after);
|
|
2068
|
+
const beforeLines = withNoNewlineMark(from);
|
|
2069
|
+
const afterLines = withNoNewlineMark(to);
|
|
2070
|
+
if (from.lines.length > limits.maxLines || to.lines.length > limits.maxLines) {
|
|
2071
|
+
return `--- ${opts.fromLabel}
|
|
2072
|
+
+++ ${opts.toLabel}
|
|
2073
|
+
@@ no line diff @@
|
|
2074
|
+
${from.lines.length} lines on disk, ${to.lines.length} lines regenerated. Not diffed: the file is longer than the ${limits.maxLines}-line cap.
|
|
2075
|
+
`;
|
|
2076
|
+
}
|
|
2077
|
+
const ops = diffLines(beforeLines, afterLines, limits.maxEdits);
|
|
2078
|
+
if (!ops) {
|
|
2079
|
+
return `--- ${opts.fromLabel}
|
|
2080
|
+
+++ ${opts.toLabel}
|
|
2081
|
+
@@ no line diff @@
|
|
2082
|
+
${from.lines.length} lines on disk, ${to.lines.length} lines regenerated. Not diffed: the two differ by more than the ${limits.maxEdits}-edit cap, so the whole file is effectively new.
|
|
2083
|
+
`;
|
|
2084
|
+
}
|
|
2085
|
+
const hunks = buildHunks(ops, beforeLines, afterLines, limits.context);
|
|
2086
|
+
if (!hunks.length) return "";
|
|
2087
|
+
return `--- ${opts.fromLabel}
|
|
2088
|
+
+++ ${opts.toLabel}
|
|
2089
|
+
${hunks.join("")}`;
|
|
2090
|
+
}
|
|
2091
|
+
var NO_NEWLINE = "\";
|
|
2092
|
+
var NO_NEWLINE_MARK = "\0\0drzl:no-newline";
|
|
2093
|
+
function withNoNewlineMark(side) {
|
|
2094
|
+
if (side.newlineAtEnd || !side.lines.length) return side.lines;
|
|
2095
|
+
const marked = side.lines.slice();
|
|
2096
|
+
marked[marked.length - 1] += NO_NEWLINE_MARK;
|
|
2097
|
+
return marked;
|
|
2098
|
+
}
|
|
2099
|
+
function renderLine(prefix, line, into) {
|
|
2100
|
+
if (line.endsWith(NO_NEWLINE_MARK)) {
|
|
2101
|
+
into.push(prefix + line.slice(0, -NO_NEWLINE_MARK.length));
|
|
2102
|
+
into.push(NO_NEWLINE);
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
into.push(prefix + line);
|
|
2106
|
+
}
|
|
2107
|
+
function buildHunks(ops, beforeLines, afterLines, context) {
|
|
2108
|
+
const changed = [];
|
|
2109
|
+
ops.forEach((op, i) => {
|
|
2110
|
+
if (op.kind !== "equal") changed.push(i);
|
|
2111
|
+
});
|
|
2112
|
+
if (!changed.length) return [];
|
|
2113
|
+
const ranges = [];
|
|
2114
|
+
for (const i of changed) {
|
|
2115
|
+
const start = Math.max(0, i - context);
|
|
2116
|
+
const end = Math.min(ops.length - 1, i + context);
|
|
2117
|
+
const last = ranges[ranges.length - 1];
|
|
2118
|
+
if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end);
|
|
2119
|
+
else ranges.push([start, end]);
|
|
2120
|
+
}
|
|
2121
|
+
const hunks = [];
|
|
2122
|
+
for (const [start, end] of ranges) {
|
|
2123
|
+
let aStart = -1;
|
|
2124
|
+
let bStart = -1;
|
|
2125
|
+
let aCount = 0;
|
|
2126
|
+
let bCount = 0;
|
|
2127
|
+
const body = [];
|
|
2128
|
+
for (let i = start; i <= end; i++) {
|
|
2129
|
+
const op = ops[i];
|
|
2130
|
+
if (op.kind === "equal" || op.kind === "delete") {
|
|
2131
|
+
if (aStart < 0) aStart = op.a;
|
|
2132
|
+
aCount++;
|
|
2133
|
+
}
|
|
2134
|
+
if (op.kind === "equal" || op.kind === "insert") {
|
|
2135
|
+
if (bStart < 0) bStart = op.b;
|
|
2136
|
+
bCount++;
|
|
2137
|
+
}
|
|
2138
|
+
if (op.kind === "equal") renderLine(" ", beforeLines[op.a], body);
|
|
2139
|
+
else if (op.kind === "delete") renderLine("-", beforeLines[op.a], body);
|
|
2140
|
+
else renderLine("+", afterLines[op.b], body);
|
|
2141
|
+
}
|
|
2142
|
+
const aFrom = aCount === 0 ? 0 : aStart + 1;
|
|
2143
|
+
const bFrom = bCount === 0 ? 0 : bStart + 1;
|
|
2144
|
+
hunks.push(`@@ -${aFrom},${aCount} +${bFrom},${bCount} @@
|
|
2145
|
+
${body.join("\n")}
|
|
2146
|
+
`);
|
|
2147
|
+
}
|
|
2148
|
+
return hunks;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// src/watch-loop.ts
|
|
2152
|
+
var DEFAULT_WATCH_DEBOUNCE_MS = 200;
|
|
2153
|
+
function resolveDebounce(value, warn) {
|
|
2154
|
+
if (value === void 0 || value === null || value === "") return DEFAULT_WATCH_DEBOUNCE_MS;
|
|
2155
|
+
const ms = Number(value);
|
|
2156
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
2157
|
+
warn(
|
|
2158
|
+
`--debounce ${String(value)} is not a number of milliseconds. Using ${DEFAULT_WATCH_DEBOUNCE_MS}ms.`
|
|
2159
|
+
);
|
|
2160
|
+
return DEFAULT_WATCH_DEBOUNCE_MS;
|
|
2161
|
+
}
|
|
2162
|
+
return ms;
|
|
2163
|
+
}
|
|
2164
|
+
function createRebuildScheduler(options) {
|
|
2165
|
+
const timers = options.timers ?? {
|
|
2166
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
2167
|
+
clearTimeout: (handle2) => clearTimeout(handle2)
|
|
2168
|
+
};
|
|
2169
|
+
let handle = null;
|
|
2170
|
+
let running = false;
|
|
2171
|
+
let pending = false;
|
|
2172
|
+
const drain = async () => {
|
|
2173
|
+
if (running) {
|
|
2174
|
+
pending = true;
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
running = true;
|
|
2178
|
+
try {
|
|
2179
|
+
await options.run();
|
|
2180
|
+
while (pending) {
|
|
2181
|
+
pending = false;
|
|
2182
|
+
await options.run();
|
|
2183
|
+
}
|
|
2184
|
+
} finally {
|
|
2185
|
+
running = false;
|
|
2186
|
+
pending = false;
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
return {
|
|
2190
|
+
trigger() {
|
|
2191
|
+
if (handle !== null) timers.clearTimeout(handle);
|
|
2192
|
+
handle = timers.setTimeout(() => {
|
|
2193
|
+
handle = null;
|
|
2194
|
+
void drain();
|
|
2195
|
+
}, options.debounceMs);
|
|
2196
|
+
},
|
|
2197
|
+
runNow() {
|
|
2198
|
+
return drain();
|
|
2199
|
+
},
|
|
2200
|
+
cancel() {
|
|
2201
|
+
if (handle !== null) timers.clearTimeout(handle);
|
|
2202
|
+
handle = null;
|
|
2203
|
+
},
|
|
2204
|
+
get busy() {
|
|
2205
|
+
return running;
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// src/init.ts
|
|
2211
|
+
import { SchemaAnalyzer } from "@drzl/analyzer";
|
|
2212
|
+
import * as fs4 from "fs";
|
|
2213
|
+
import * as path4 from "path";
|
|
2214
|
+
var INIT_GENERATOR_CHOICES = [
|
|
2215
|
+
{ kind: "zod", packageName: "@drzl/generator-zod", label: "Zod validators" },
|
|
2216
|
+
{ kind: "valibot", packageName: "@drzl/generator-valibot", label: "Valibot validators" },
|
|
2217
|
+
{ kind: "arktype", packageName: "@drzl/generator-arktype", label: "ArkType validators" },
|
|
2218
|
+
{ kind: "typebox", packageName: "@drzl/generator-typebox", label: "TypeBox validators" },
|
|
2219
|
+
{ kind: "orpc", packageName: "@drzl/generator-orpc", label: "oRPC router" }
|
|
2220
|
+
];
|
|
2221
|
+
var DEFAULT_GENERATOR_KIND = INIT_GENERATOR_CHOICES[0].kind;
|
|
2222
|
+
var ROUTER_KINDS = /* @__PURE__ */ new Set(["orpc"]);
|
|
2223
|
+
var SCHEMA_CANDIDATE_STEMS = [
|
|
2224
|
+
"src/db/schema",
|
|
2225
|
+
"src/db/schema/index",
|
|
2226
|
+
"src/db/schemas/index",
|
|
2227
|
+
"src/lib/db/schema",
|
|
2228
|
+
"src/lib/db/schema/index",
|
|
2229
|
+
"src/schema",
|
|
2230
|
+
"src/schema/index",
|
|
2231
|
+
"src/schemas/index",
|
|
2232
|
+
"app/db/schema",
|
|
2233
|
+
"lib/db/schema",
|
|
2234
|
+
"db/schema",
|
|
2235
|
+
"db/schema/index",
|
|
2236
|
+
"drizzle/schema",
|
|
2237
|
+
"schema"
|
|
2238
|
+
];
|
|
2239
|
+
var CANDIDATE_EXTENSIONS = [".ts", ".js"];
|
|
2240
|
+
function schemaCandidates() {
|
|
2241
|
+
const out = [];
|
|
2242
|
+
for (const stem of SCHEMA_CANDIDATE_STEMS) {
|
|
2243
|
+
for (const ext of CANDIDATE_EXTENSIONS) out.push(`${stem}${ext}`);
|
|
2244
|
+
}
|
|
2245
|
+
return out;
|
|
2246
|
+
}
|
|
2247
|
+
async function classifySchemaCandidate(target) {
|
|
2248
|
+
let analysis;
|
|
2249
|
+
try {
|
|
2250
|
+
analysis = await new SchemaAnalyzer(target).analyze({
|
|
2251
|
+
includeRelations: false,
|
|
2252
|
+
validateConstraints: false
|
|
2253
|
+
});
|
|
2254
|
+
} catch (e) {
|
|
2255
|
+
return { verdict: "unverified", tables: 0, reason: firstLine2(String(e?.message ?? e)) };
|
|
2256
|
+
}
|
|
2257
|
+
if (analysis.tables.length > 0) {
|
|
2258
|
+
return { verdict: "confirmed", tables: analysis.tables.length };
|
|
2259
|
+
}
|
|
2260
|
+
const importError = analysis.issues.find(
|
|
2261
|
+
(i) => i.level === "error" && i.code === "DRZL_ANL_IMPORT"
|
|
2262
|
+
);
|
|
2263
|
+
if (importError)
|
|
2264
|
+
return { verdict: "unverified", tables: 0, reason: firstLine2(importError.message) };
|
|
2265
|
+
return { verdict: "rejected", tables: 0 };
|
|
2266
|
+
}
|
|
2267
|
+
function firstLine2(message) {
|
|
2268
|
+
return String(message).split("\n")[0].trim();
|
|
2269
|
+
}
|
|
2270
|
+
async function detectSchema(cwd) {
|
|
2271
|
+
const notes = [];
|
|
2272
|
+
let kitFiles = null;
|
|
2273
|
+
let kitPath;
|
|
2274
|
+
try {
|
|
2275
|
+
const source = await resolveSchemaSource({}, cwd);
|
|
2276
|
+
if (source.source === "drizzle-kit") {
|
|
2277
|
+
kitFiles = source.schema;
|
|
2278
|
+
kitPath = source.drizzleKitConfigPath;
|
|
2279
|
+
}
|
|
2280
|
+
} catch {
|
|
2281
|
+
kitFiles = null;
|
|
2282
|
+
}
|
|
2283
|
+
if (kitFiles && kitPath) {
|
|
2284
|
+
const rel = path4.relative(cwd, kitPath) || path4.basename(kitPath);
|
|
2285
|
+
const report = await classifySchemaCandidate(kitFiles);
|
|
2286
|
+
if (report.verdict === "confirmed" || report.verdict === "unverified") {
|
|
2287
|
+
notes.push(
|
|
2288
|
+
report.verdict === "confirmed" ? `Schema from ${rel} (${kitFiles.length} file${kitFiles.length === 1 ? "" : "s"}, ${report.tables} table${report.tables === 1 ? "" : "s"})` : `Schema from ${rel}, which DRZL could not import yet: ${report.reason}`
|
|
2289
|
+
);
|
|
2290
|
+
return {
|
|
2291
|
+
source: "drizzle-kit",
|
|
2292
|
+
drizzleKitConfig: rel,
|
|
2293
|
+
verdict: report.verdict,
|
|
2294
|
+
tables: report.tables,
|
|
2295
|
+
notes
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
notes.push(`${rel} names schema files that declare no Drizzle tables; looking elsewhere.`);
|
|
2299
|
+
}
|
|
2300
|
+
const present = schemaCandidates().filter((c) => fs4.existsSync(path4.resolve(cwd, c)));
|
|
2301
|
+
const unverified = [];
|
|
2302
|
+
for (const file of present) {
|
|
2303
|
+
const report = await classifySchemaCandidate(path4.resolve(cwd, file));
|
|
2304
|
+
if (report.verdict === "confirmed") {
|
|
2305
|
+
notes.push(
|
|
2306
|
+
`Schema found at ${file} (${report.tables} table${report.tables === 1 ? "" : "s"})`
|
|
2307
|
+
);
|
|
2308
|
+
return {
|
|
2309
|
+
source: "convention",
|
|
2310
|
+
schema: file,
|
|
2311
|
+
verdict: "confirmed",
|
|
2312
|
+
tables: report.tables,
|
|
2313
|
+
notes
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
if (report.verdict === "unverified") unverified.push({ file, report });
|
|
2317
|
+
else notes.push(`${file} exists but declares no Drizzle tables; not using it.`);
|
|
2318
|
+
}
|
|
2319
|
+
if (unverified.length) {
|
|
2320
|
+
const { file, report } = unverified[0];
|
|
2321
|
+
notes.push(`Schema assumed to be ${file}; DRZL could not import it: ${report.reason}`);
|
|
2322
|
+
return { source: "convention", schema: file, verdict: "unverified", tables: 0, notes };
|
|
2323
|
+
}
|
|
2324
|
+
notes.push(
|
|
2325
|
+
present.length ? "No file DRZL looked at declares any Drizzle tables." : "No drizzle-kit config and no schema in the usual locations."
|
|
2326
|
+
);
|
|
2327
|
+
return { source: "none", tables: 0, notes };
|
|
2328
|
+
}
|
|
2329
|
+
function generatorLine(kind) {
|
|
2330
|
+
if (kind === "orpc") return `{ kind: 'orpc', template: 'standard', includeRelations: true }`;
|
|
2331
|
+
return `{ kind: '${kind}', path: 'src/validators/${kind}' }`;
|
|
2332
|
+
}
|
|
2333
|
+
function renderInitConfig(plan) {
|
|
2334
|
+
const lines = [];
|
|
2335
|
+
lines.push(`import type { DrzlConfigInput } from '@drzl/cli/config';`);
|
|
2336
|
+
lines.push("");
|
|
2337
|
+
lines.push("export default {");
|
|
2338
|
+
if (plan.schema) {
|
|
2339
|
+
lines.push(` schema: '${plan.schema}',`);
|
|
2340
|
+
} else if (plan.schemaSource === "drizzle-kit") {
|
|
2341
|
+
lines.push(` // No "schema" here on purpose: DRZL reads it from your drizzle-kit config, so`);
|
|
2342
|
+
lines.push(
|
|
2343
|
+
` // the path is written once. Set "schema" to override it, or "drizzleKit": false`
|
|
2344
|
+
);
|
|
2345
|
+
lines.push(` // to refuse the fallback.`);
|
|
2346
|
+
} else {
|
|
2347
|
+
lines.push(` // Set this to your Drizzle schema file, for example 'src/db/schema.ts'. DRZL`);
|
|
2348
|
+
lines.push(` // found no drizzle-kit config and no schema declaring tables in the usual`);
|
|
2349
|
+
lines.push(` // locations, and will not name a file that is not there.`);
|
|
2350
|
+
lines.push(` // schema: 'src/db/schema.ts',`);
|
|
2351
|
+
}
|
|
2352
|
+
const hasRouter = plan.generators.some((k) => ROUTER_KINDS.has(k));
|
|
2353
|
+
if (hasRouter) lines.push(` outDir: 'src/api',`);
|
|
2354
|
+
lines.push(` analyzer: { includeRelations: true, validateConstraints: true },`);
|
|
2355
|
+
lines.push(" generators: [");
|
|
2356
|
+
const others = INIT_GENERATOR_CHOICES.filter((c) => !plan.generators.includes(c.kind)).map((c) => `'${c.kind}'`).join(", ");
|
|
2357
|
+
if (others) lines.push(` // Other kinds this CLI already has installed: ${others}.`);
|
|
2358
|
+
if (hasRouter) {
|
|
2359
|
+
lines.push(' // A second router generator needs its own "path"; they share "outDir".');
|
|
2360
|
+
}
|
|
2361
|
+
for (const kind of plan.generators) lines.push(` ${generatorLine(kind)},`);
|
|
2362
|
+
lines.push(" ],");
|
|
2363
|
+
lines.push("} satisfies DrzlConfigInput;");
|
|
2364
|
+
return lines.join("\n") + "\n";
|
|
2365
|
+
}
|
|
2366
|
+
function isInteractive(ctx) {
|
|
2367
|
+
if (ctx.env.CI) return false;
|
|
2368
|
+
return Boolean(ctx.stdin.isTTY) && Boolean(ctx.stdout.isTTY);
|
|
2369
|
+
}
|
|
2370
|
+
async function ask(rl, question) {
|
|
2371
|
+
const closed = new Promise((resolve4) => rl.once("close", () => resolve4(null)));
|
|
2372
|
+
try {
|
|
2373
|
+
return await Promise.race([rl.question(question), closed]);
|
|
2374
|
+
} catch {
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
async function promptForPlan(args) {
|
|
2379
|
+
const { input, output, detection, cwd } = args;
|
|
2380
|
+
const write = (s) => output.write(s + "\n");
|
|
2381
|
+
let schema = args.schemaFromFlag ?? detection.schema;
|
|
2382
|
+
let schemaSource = args.schemaFromFlag ? "convention" : detection.source;
|
|
2383
|
+
let generators = args.generatorsFromFlag;
|
|
2384
|
+
let endedEarly = false;
|
|
2385
|
+
let readlineModule;
|
|
2386
|
+
try {
|
|
2387
|
+
readlineModule = await import("readline/promises");
|
|
2388
|
+
} catch {
|
|
2389
|
+
return {
|
|
2390
|
+
schema,
|
|
2391
|
+
schemaSource,
|
|
2392
|
+
generators: normalizeGenerators(generators) ?? [DEFAULT_GENERATOR_KIND],
|
|
2393
|
+
endedEarly: true
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
const rl = readlineModule.createInterface({ input, output });
|
|
2397
|
+
try {
|
|
2398
|
+
if (args.schemaFromFlag === void 0) {
|
|
2399
|
+
for (const note of detection.notes) write(note);
|
|
2400
|
+
const prompt = detection.source === "drizzle-kit" ? "Schema file, or Enter to keep reading it from your drizzle-kit config: " : detection.schema ? `Schema file [${detection.schema}]: ` : "Schema file (Enter to leave it unset): ";
|
|
2401
|
+
const answer = await ask(rl, prompt);
|
|
2402
|
+
if (answer === null) endedEarly = true;
|
|
2403
|
+
else if (answer.trim()) {
|
|
2404
|
+
const typed = answer.trim();
|
|
2405
|
+
const report = await classifySchemaCandidate(path4.resolve(cwd, typed));
|
|
2406
|
+
if (report.verdict === "confirmed") {
|
|
2407
|
+
write(` ${typed}: ${report.tables} table${report.tables === 1 ? "" : "s"}`);
|
|
2408
|
+
} else if (report.verdict === "unverified") {
|
|
2409
|
+
write(` ${typed}: DRZL could not import it (${report.reason}). Using it anyway.`);
|
|
2410
|
+
} else {
|
|
2411
|
+
write(` ${typed}: no Drizzle tables found in it. Using it anyway.`);
|
|
2412
|
+
}
|
|
2413
|
+
schema = typed;
|
|
2414
|
+
schemaSource = "convention";
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
if (generators === void 0 && !endedEarly) {
|
|
2418
|
+
write("What should DRZL generate?");
|
|
2419
|
+
INIT_GENERATOR_CHOICES.forEach((c, i) => write(` ${i + 1}) ${c.label}`));
|
|
2420
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
2421
|
+
const answer = await ask(rl, `Choice [1, ${INIT_GENERATOR_CHOICES[0].label}]: `);
|
|
2422
|
+
if (answer === null) {
|
|
2423
|
+
endedEarly = true;
|
|
2424
|
+
break;
|
|
2425
|
+
}
|
|
2426
|
+
const raw = answer.trim().toLowerCase();
|
|
2427
|
+
if (!raw) break;
|
|
2428
|
+
const byIndex = Number(raw);
|
|
2429
|
+
const picked = Number.isInteger(byIndex) && byIndex >= 1 && byIndex <= INIT_GENERATOR_CHOICES.length ? INIT_GENERATOR_CHOICES[byIndex - 1] : INIT_GENERATOR_CHOICES.find((c) => c.kind === raw);
|
|
2430
|
+
if (picked) {
|
|
2431
|
+
generators = [picked.kind];
|
|
2432
|
+
break;
|
|
2433
|
+
}
|
|
2434
|
+
write(` "${answer.trim()}" is not one of the choices.`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
} finally {
|
|
2438
|
+
rl.close();
|
|
2439
|
+
}
|
|
2440
|
+
return {
|
|
2441
|
+
schema,
|
|
2442
|
+
schemaSource,
|
|
2443
|
+
generators: normalizeGenerators(generators) ?? [DEFAULT_GENERATOR_KIND],
|
|
2444
|
+
endedEarly
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
function normalizeGenerators(kinds) {
|
|
2448
|
+
if (kinds === void 0) return void 0;
|
|
2449
|
+
const known = new Set(INIT_GENERATOR_CHOICES.map((c) => c.kind));
|
|
2450
|
+
for (const k of kinds) {
|
|
2451
|
+
if (!known.has(k)) {
|
|
2452
|
+
throw new Error(
|
|
2453
|
+
`drzl init: "${k}" is not a generator init can scaffold. Choose from ${[...known].join(", ")}. Every other kind is installed and works; add it to drzl.config by hand, following the entry for it in the docs.`
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
const picked = INIT_GENERATOR_CHOICES.filter((c) => kinds.includes(c.kind)).map((c) => c.kind);
|
|
2458
|
+
return picked.length ? picked : void 0;
|
|
512
2459
|
}
|
|
513
|
-
|
|
2460
|
+
function parseGeneratorsFlag(value) {
|
|
2461
|
+
if (value === void 0) return void 0;
|
|
2462
|
+
const parts = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
2463
|
+
if (!parts.length) throw new Error("drzl init: --generators was given no kinds.");
|
|
2464
|
+
return parts;
|
|
2465
|
+
}
|
|
2466
|
+
async function runInit(args) {
|
|
2467
|
+
const target = path4.resolve(args.cwd, "drzl.config.ts");
|
|
2468
|
+
const existing = CONFIG_FILE_NAMES.find((name) => fs4.existsSync(path4.resolve(args.cwd, name)));
|
|
2469
|
+
if (existing) {
|
|
2470
|
+
args.error(
|
|
2471
|
+
`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.`
|
|
2472
|
+
);
|
|
2473
|
+
return { code: 1 };
|
|
2474
|
+
}
|
|
2475
|
+
let fromFlag;
|
|
514
2476
|
try {
|
|
515
|
-
|
|
2477
|
+
fromFlag = normalizeGenerators(parseGeneratorsFlag(args.generatorsFlag));
|
|
516
2478
|
} catch (e) {
|
|
517
|
-
|
|
518
|
-
|
|
2479
|
+
args.error(String(e?.message ?? e));
|
|
2480
|
+
return { code: 1 };
|
|
2481
|
+
}
|
|
2482
|
+
const detection = await detectSchema(args.cwd);
|
|
2483
|
+
let plan;
|
|
2484
|
+
const interactive = !args.yes && isInteractive({ stdin: args.stdin, stdout: args.stdout, env: args.env });
|
|
2485
|
+
if (interactive) {
|
|
2486
|
+
const result = await promptForPlan({
|
|
2487
|
+
input: args.stdin,
|
|
2488
|
+
output: args.stdout,
|
|
2489
|
+
detection,
|
|
2490
|
+
cwd: args.cwd,
|
|
2491
|
+
schemaFromFlag: args.schemaFlag,
|
|
2492
|
+
generatorsFromFlag: fromFlag
|
|
2493
|
+
});
|
|
2494
|
+
plan = {
|
|
2495
|
+
schema: result.schema,
|
|
2496
|
+
schemaSource: result.schemaSource,
|
|
2497
|
+
generators: result.generators
|
|
2498
|
+
};
|
|
2499
|
+
} else {
|
|
2500
|
+
for (const note of detection.notes) args.log(note);
|
|
2501
|
+
plan = {
|
|
2502
|
+
schema: args.schemaFlag ?? detection.schema,
|
|
2503
|
+
schemaSource: args.schemaFlag ? "convention" : detection.source,
|
|
2504
|
+
generators: fromFlag ?? [DEFAULT_GENERATOR_KIND]
|
|
2505
|
+
};
|
|
2506
|
+
if (args.schemaFlag) {
|
|
2507
|
+
const full = path4.resolve(args.cwd, args.schemaFlag);
|
|
2508
|
+
if (!fs4.existsSync(full)) {
|
|
2509
|
+
args.log(`--schema ${args.schemaFlag} is not there yet. Writing it anyway.`);
|
|
2510
|
+
} else if ((await classifySchemaCandidate(full)).verdict === "rejected") {
|
|
2511
|
+
args.log(`--schema ${args.schemaFlag} declares no Drizzle tables. Writing it anyway.`);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
try {
|
|
2516
|
+
fs4.writeFileSync(target, renderInitConfig(plan), { flag: "wx" });
|
|
2517
|
+
} catch (e) {
|
|
2518
|
+
if (e?.code === "EEXIST") {
|
|
2519
|
+
args.error(
|
|
2520
|
+
`drzl init: drzl.config.ts already exists, so nothing was written. init never overwrites a config.`
|
|
2521
|
+
);
|
|
2522
|
+
return { code: 1 };
|
|
2523
|
+
}
|
|
2524
|
+
args.error(`drzl init: could not write ${target}: ${e?.message ?? e}`);
|
|
2525
|
+
return { code: 1 };
|
|
519
2526
|
}
|
|
2527
|
+
args.log(`Created ${target}`);
|
|
2528
|
+
args.log(` generators: ${plan.generators.join(", ")}`);
|
|
2529
|
+
if (plan.schema) args.log(` schema: ${plan.schema}`);
|
|
2530
|
+
else if (plan.schemaSource === "drizzle-kit") args.log(" schema: from your drizzle-kit config");
|
|
2531
|
+
else
|
|
2532
|
+
args.log(
|
|
2533
|
+
' schema: not set. Fill in "schema" before running `drzl generate`, or add a drizzle-kit config.'
|
|
2534
|
+
);
|
|
2535
|
+
return { code: 0, written: target, plan };
|
|
520
2536
|
}
|
|
521
2537
|
|
|
522
2538
|
// src/sponsor.ts
|
|
523
|
-
import
|
|
524
|
-
import
|
|
525
|
-
|
|
526
|
-
var
|
|
527
|
-
var CACHE_FILE = path2.join(CACHE_DIR, "sponsor-message.json");
|
|
2539
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2540
|
+
import path5 from "path";
|
|
2541
|
+
var CACHE_DIR = path5.join(process.cwd(), "node_modules", ".cache", "@drzl");
|
|
2542
|
+
var CACHE_FILE = path5.join(CACHE_DIR, "sponsor-message.json");
|
|
528
2543
|
var DEFAULT_INTERVAL_MS = 1e3 * 60 * 15;
|
|
529
2544
|
var shownThisProcess = false;
|
|
530
2545
|
var tips = [
|
|
@@ -534,17 +2549,19 @@ var tips = [
|
|
|
534
2549
|
"Need JSON Schema or OpenAPI? The json-schema generator emits both, with no runtime dependency.",
|
|
535
2550
|
"Use output headers to track generated files and trim noisy diffs."
|
|
536
2551
|
];
|
|
537
|
-
var green = (msg) => chalk2.hex("#6ee7b7")(msg);
|
|
538
|
-
var cyan = (msg) => chalk2.cyan(msg);
|
|
539
|
-
var gray = (msg) => chalk2.gray(msg);
|
|
540
2552
|
function maybeShowSponsorMessage({
|
|
541
2553
|
reason = "generate",
|
|
542
2554
|
minIntervalMs = DEFAULT_INTERVAL_MS,
|
|
543
|
-
force = false
|
|
2555
|
+
force = false,
|
|
2556
|
+
out = new Output()
|
|
544
2557
|
} = {}) {
|
|
2558
|
+
const green = (msg) => out.errStyle.hex("#6ee7b7")(msg);
|
|
2559
|
+
const cyan = (msg) => out.errStyle.cyan(msg);
|
|
2560
|
+
const gray = (msg) => out.errStyle.gray(msg);
|
|
545
2561
|
const hideViaEnv = process.env.DRZL_HIDE_SPONSOR?.toLowerCase();
|
|
546
2562
|
const hideRequested = hideViaEnv === "1" || hideViaEnv === "true";
|
|
547
2563
|
if (hideRequested || process.env.CI && !force || shownThisProcess && !force) return;
|
|
2564
|
+
if (!out.wantsAsides && !force) return;
|
|
548
2565
|
try {
|
|
549
2566
|
mkdirSync(CACHE_DIR, { recursive: true });
|
|
550
2567
|
const payload = readCache();
|
|
@@ -559,7 +2576,7 @@ function maybeShowSponsorMessage({
|
|
|
559
2576
|
if (!shouldShow) return;
|
|
560
2577
|
shownThisProcess = true;
|
|
561
2578
|
const tip = tips[payload.runs % tips.length];
|
|
562
|
-
|
|
2579
|
+
out.stderr.write(
|
|
563
2580
|
`
|
|
564
2581
|
${cyan(`\u{1F680} DRZL finished a ${reason} run (#${payload.runs.toLocaleString()}).`)}
|
|
565
2582
|
|
|
@@ -567,13 +2584,14 @@ ${green("\u2728 Sponsors keep DRZL shipping. Consider supporting ongoing dev:")}
|
|
|
567
2584
|
${green("GitHub Sponsors")} ${gray("\u2192 https://github.com/sponsors/omar-dulaimi")}
|
|
568
2585
|
|
|
569
2586
|
${green("Pro tip:")} ${tip}
|
|
2587
|
+
|
|
570
2588
|
`
|
|
571
2589
|
);
|
|
572
2590
|
} catch {
|
|
573
2591
|
}
|
|
574
2592
|
}
|
|
575
2593
|
function readCache() {
|
|
576
|
-
if (!
|
|
2594
|
+
if (!existsSync3(CACHE_FILE)) {
|
|
577
2595
|
return { runs: 0 };
|
|
578
2596
|
}
|
|
579
2597
|
try {
|
|
@@ -585,16 +2603,16 @@ function readCache() {
|
|
|
585
2603
|
}
|
|
586
2604
|
}
|
|
587
2605
|
function writeCache(payload) {
|
|
588
|
-
|
|
2606
|
+
writeFileSync2(CACHE_FILE, JSON.stringify(payload, null, 2), "utf8");
|
|
589
2607
|
}
|
|
590
2608
|
|
|
591
2609
|
// src/version.ts
|
|
592
2610
|
import { readFileSync as readFileSync2 } from "fs";
|
|
593
|
-
import * as
|
|
2611
|
+
import * as path6 from "path";
|
|
594
2612
|
import { fileURLToPath } from "url";
|
|
595
2613
|
var PACKAGE_NAME = "@drzl/cli";
|
|
596
2614
|
function moduleDir() {
|
|
597
|
-
return
|
|
2615
|
+
return path6.dirname(fileURLToPath(import.meta.url));
|
|
598
2616
|
}
|
|
599
2617
|
function readVersionFrom(manifestPath) {
|
|
600
2618
|
let raw;
|
|
@@ -617,21 +2635,66 @@ function readVersionFrom(manifestPath) {
|
|
|
617
2635
|
return manifest.version;
|
|
618
2636
|
}
|
|
619
2637
|
function readCliVersion() {
|
|
620
|
-
return readVersionFrom(
|
|
2638
|
+
return readVersionFrom(path6.join(moduleDir(), "..", "package.json"));
|
|
621
2639
|
}
|
|
622
2640
|
var CLI_VERSION = readCliVersion();
|
|
623
2641
|
|
|
624
2642
|
// src/cli.ts
|
|
625
|
-
function reportGeneratorFailure(kind, e) {
|
|
2643
|
+
function reportGeneratorFailure(out, kind, e) {
|
|
626
2644
|
if (e instanceof GeneratorNotInstalledError) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
2645
|
+
out.error(`The ${kind} generator is not installed.`);
|
|
2646
|
+
out.hint(`Install with: npm install ${e.specifier}`);
|
|
2647
|
+
return `The ${kind} generator is not installed. Install with: npm install ${e.specifier}`;
|
|
2648
|
+
}
|
|
2649
|
+
const detail = messageOf(e);
|
|
2650
|
+
out.error(`The ${kind} generator failed:`, detail);
|
|
2651
|
+
return `The ${kind} generator failed: ${detail}`;
|
|
2652
|
+
}
|
|
2653
|
+
function outputFor(opts) {
|
|
2654
|
+
return new Output({ quiet: !!opts.quiet, json: !!opts.json });
|
|
2655
|
+
}
|
|
2656
|
+
function drzlErrorCode(error, fallback) {
|
|
2657
|
+
return error instanceof ConfigValidationError ? error.code : fallback;
|
|
2658
|
+
}
|
|
2659
|
+
function reportSchemaProblem(out, command, problem) {
|
|
2660
|
+
if (out.json) out.jsonData(jsonFailure(command, problem.code, problem.message));
|
|
2661
|
+
else {
|
|
2662
|
+
out.error(problem.message);
|
|
2663
|
+
out.hint(problem.hint);
|
|
2664
|
+
}
|
|
2665
|
+
process.exit(EXIT_FAILED);
|
|
2666
|
+
}
|
|
2667
|
+
var DIFF_FILE_CAP = 20;
|
|
2668
|
+
function printCheckDiffs(out, drift) {
|
|
2669
|
+
const shown = drift.slice(0, DIFF_FILE_CAP);
|
|
2670
|
+
for (const d of shown) {
|
|
2671
|
+
const label = displayPath(d.file);
|
|
2672
|
+
const text = unifiedDiff(d.before ?? "", d.after, {
|
|
2673
|
+
fromLabel: `a/${label}`,
|
|
2674
|
+
toLabel: `b/${label}`
|
|
2675
|
+
});
|
|
2676
|
+
if (!text) continue;
|
|
2677
|
+
out.note("");
|
|
2678
|
+
for (const line of text.split("\n")) {
|
|
2679
|
+
if (!line) continue;
|
|
2680
|
+
if (line.startsWith("+++") || line.startsWith("---")) out.note(out.errStyle.bold(line));
|
|
2681
|
+
else if (line.startsWith("@@")) out.note(out.errStyle.cyan(line));
|
|
2682
|
+
else if (line.startsWith("+")) out.note(out.errStyle.green(line));
|
|
2683
|
+
else if (line.startsWith("-")) out.note(out.errStyle.red(line));
|
|
2684
|
+
else out.note(out.errStyle.gray(line));
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
if (drift.length > shown.length) {
|
|
2688
|
+
out.note("");
|
|
2689
|
+
out.note(
|
|
2690
|
+
out.errStyle.gray(
|
|
2691
|
+
`${drift.length - shown.length} more file(s) differ. Diffs are capped at ${DIFF_FILE_CAP} files; every drifted file is named in the list above.`
|
|
2692
|
+
)
|
|
631
2693
|
);
|
|
632
|
-
return;
|
|
633
2694
|
}
|
|
634
|
-
|
|
2695
|
+
}
|
|
2696
|
+
function withOutputFlags(command) {
|
|
2697
|
+
return command.option("--json", "write one JSON document to stdout and nothing else", false).option("-q, --quiet", "drop the progress narration on stderr; errors still print", false);
|
|
635
2698
|
}
|
|
636
2699
|
var program = new Command();
|
|
637
2700
|
program.name("drzl").description("DRZL - Drizzle Developer Toolkit").version(CLI_VERSION);
|
|
@@ -642,374 +2705,531 @@ Need a template, adapter, or generator DRZL doesn't ship yet?
|
|
|
642
2705
|
\u2192 DM @omardulaimidev on X: https://x.com/omardulaimidev
|
|
643
2706
|
`
|
|
644
2707
|
);
|
|
645
|
-
|
|
2708
|
+
withOutputFlags(
|
|
2709
|
+
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")
|
|
2710
|
+
).action(async (schema, opts) => {
|
|
2711
|
+
const out = outputFor(opts);
|
|
646
2712
|
try {
|
|
647
|
-
const analyzer = new
|
|
648
|
-
const spinner =
|
|
2713
|
+
const analyzer = new SchemaAnalyzer2(schema);
|
|
2714
|
+
const spinner = out.spinner("Analyzing schema...");
|
|
649
2715
|
const start = Date.now();
|
|
650
2716
|
const res = await analyzer.analyze({
|
|
651
2717
|
includeRelations: !!opts.relations,
|
|
652
2718
|
validateConstraints: !!opts.validate
|
|
653
2719
|
});
|
|
654
2720
|
const ms = Date.now() - start;
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
2721
|
+
const unreadable = res.issues.some(
|
|
2722
|
+
(i) => i.level === "error" && (i.code === "DRZL_ANL_NOFILE" || i.code === "DRZL_ANL_IMPORT")
|
|
2723
|
+
);
|
|
2724
|
+
const errors = res.issues.some((i) => i.level === "error");
|
|
2725
|
+
const code = unreadable ? EXIT_FAILED : errors ? EXIT_FINDINGS : EXIT_OK;
|
|
2726
|
+
if (opts.out && !opts.json) {
|
|
2727
|
+
const fs5 = await import("fs/promises");
|
|
2728
|
+
await fs5.writeFile(opts.out, JSON.stringify(res, null, 2), "utf8");
|
|
2729
|
+
spinner.succeed(`Analysis written to ${opts.out} in ${ms}ms`);
|
|
662
2730
|
} else {
|
|
663
|
-
spinner
|
|
664
|
-
|
|
2731
|
+
spinner.succeed(`Analyzed in ${ms}ms`);
|
|
2732
|
+
const document = opts.json ? { command: "analyze", exitCode: code, ...res } : res;
|
|
2733
|
+
out.data(JSON.stringify(document, null, 2));
|
|
665
2734
|
}
|
|
666
|
-
process.exit(
|
|
2735
|
+
process.exit(code);
|
|
667
2736
|
} catch (e) {
|
|
668
|
-
const msg =
|
|
669
|
-
if (opts.json)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
2737
|
+
const msg = messageOf(e);
|
|
2738
|
+
if (opts.json) out.jsonData(jsonFailure("analyze", "DRZL_CLI_ANALYZE", msg));
|
|
2739
|
+
else {
|
|
2740
|
+
out.error("Analyze failed (DRZL_CLI_ANALYZE):", msg);
|
|
2741
|
+
out.hint("Tip: run with --json for structured output.");
|
|
2742
|
+
}
|
|
2743
|
+
process.exit(EXIT_FAILED);
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2746
|
+
withOutputFlags(
|
|
2747
|
+
program.command("doctor").description("Report what DRZL cannot type or enforce in your schema, and why").argument("[schema]", "path to drizzle schema (TS); defaults to the schema in drzl.config").option("-c, --config <path>", "path to drzl.config, read when no schema argument is given").option("--strict", "exit 2 when anything is reported", false)
|
|
2748
|
+
).action(async (schema, opts) => {
|
|
2749
|
+
const out = outputFor(opts);
|
|
2750
|
+
{
|
|
2751
|
+
try {
|
|
2752
|
+
let target = schema;
|
|
2753
|
+
if (!target) {
|
|
2754
|
+
const cfg = await loadConfig(opts.config, (w) => out.warn(w));
|
|
2755
|
+
if (cfg) target = (await resolveSchemaSource(cfg)).schema;
|
|
2756
|
+
}
|
|
2757
|
+
if (!target) {
|
|
2758
|
+
const msg = "No schema given. Pass a path, or run from a directory with a drzl.config.";
|
|
2759
|
+
if (opts.json) out.jsonData(jsonFailure("doctor", "DRZL_CLI_DOCTOR", msg));
|
|
2760
|
+
else out.error("Doctor failed (DRZL_CLI_DOCTOR):", msg);
|
|
2761
|
+
process.exit(EXIT_FAILED);
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
const analyzer = new SchemaAnalyzer2(target);
|
|
2765
|
+
const analysis = await analyzer.analyze({
|
|
2766
|
+
includeRelations: true,
|
|
2767
|
+
validateConstraints: true
|
|
2768
|
+
});
|
|
2769
|
+
const report = buildDoctorReport(
|
|
2770
|
+
analysis,
|
|
2771
|
+
Array.isArray(target) ? target.join(", ") : target
|
|
676
2772
|
);
|
|
677
|
-
|
|
2773
|
+
const unreadable = report.findings.some((f) => f.level === "error");
|
|
2774
|
+
const code = unreadable ? EXIT_FAILED : opts.strict && report.findings.length ? EXIT_FINDINGS : EXIT_OK;
|
|
2775
|
+
if (opts.json)
|
|
2776
|
+
out.data(JSON.stringify({ command: "doctor", exitCode: code, ...report }, null, 2));
|
|
2777
|
+
else out.data(renderDoctorReport(report, out.outStyle));
|
|
2778
|
+
process.exit(code);
|
|
2779
|
+
} catch (e) {
|
|
2780
|
+
const msg = messageOf(e);
|
|
2781
|
+
const code = drzlErrorCode(e, "DRZL_CLI_DOCTOR");
|
|
2782
|
+
if (opts.json) out.jsonData(jsonFailure("doctor", code, msg));
|
|
2783
|
+
else if (e instanceof ConfigValidationError) {
|
|
2784
|
+
out.error(msg);
|
|
2785
|
+
} else {
|
|
2786
|
+
out.error("Doctor failed (DRZL_CLI_DOCTOR):", msg);
|
|
2787
|
+
out.hint("Tip: run with --json for structured output.");
|
|
2788
|
+
}
|
|
2789
|
+
process.exit(EXIT_FAILED);
|
|
2790
|
+
}
|
|
678
2791
|
}
|
|
679
2792
|
});
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
2793
|
+
async function explainSchemaSource(opts, out) {
|
|
2794
|
+
if (opts.schema) return { schema: opts.schema, label: opts.schema };
|
|
2795
|
+
const cfg = await loadConfig(opts.config, (w) => out.warn(w));
|
|
2796
|
+
if (cfg) {
|
|
2797
|
+
const source = await resolveSchemaSource(cfg);
|
|
2798
|
+
for (const w of source.warnings) out.warn(w);
|
|
2799
|
+
return {
|
|
2800
|
+
schema: source.schema,
|
|
2801
|
+
label: describeSchemaTarget(source.schema),
|
|
2802
|
+
config: cfg,
|
|
2803
|
+
...source.source === "drizzle-kit" && source.drizzleKitConfigPath ? { note: `Schema from ${path7.relative(process.cwd(), source.drizzleKitConfigPath)}` } : {}
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
const detected = await detectSchema(process.cwd());
|
|
2807
|
+
if (!detected.schema) return void 0;
|
|
2808
|
+
return {
|
|
2809
|
+
schema: detected.schema,
|
|
2810
|
+
label: detected.schema,
|
|
2811
|
+
note: detected.notes[detected.notes.length - 1]
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
withOutputFlags(
|
|
2815
|
+
program.command("explain").description("Show what DRZL understood about one table, and what it did not").argument(
|
|
2816
|
+
"[table]",
|
|
2817
|
+
"the table to explain, by database name, qualified name or export name; omit for the list"
|
|
2818
|
+
).option("-c, --config <path>", "path to drzl.config, read when --schema is not given").option("-s, --schema <path>", "path to the schema, overriding the config")
|
|
2819
|
+
).action(async (tableName, opts) => {
|
|
2820
|
+
const out = outputFor(opts);
|
|
2821
|
+
const fail = (problem) => {
|
|
2822
|
+
if (out.json) out.jsonData(jsonFailure("explain", problem.code, problem.message));
|
|
2823
|
+
else {
|
|
2824
|
+
out.error(problem.message);
|
|
2825
|
+
out.hint(problem.hint);
|
|
686
2826
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
2827
|
+
process.exit(EXIT_FAILED);
|
|
2828
|
+
};
|
|
2829
|
+
try {
|
|
2830
|
+
const source = await explainSchemaSource(opts, out);
|
|
2831
|
+
if (!source) {
|
|
2832
|
+
fail({
|
|
2833
|
+
code: "DRZL_CFG_001",
|
|
2834
|
+
message: "No schema found (DRZL_CFG_001). There is no drzl.config, no drizzle-kit config, and no schema in the usual locations.",
|
|
2835
|
+
hint: "Pass --schema <path>, or run `drzl init` to write a config."
|
|
2836
|
+
});
|
|
693
2837
|
return;
|
|
694
2838
|
}
|
|
695
|
-
|
|
696
|
-
const
|
|
2839
|
+
if (source.note) out.note(out.errStyle.gray(source.note));
|
|
2840
|
+
const spinner = out.spinner("Reading the schema...");
|
|
2841
|
+
const analysis = await new SchemaAnalyzer2(source.schema).analyze({
|
|
2842
|
+
// Both on, for the reason `doctor` turns both on: this command's job is to say everything
|
|
2843
|
+
// that is known, and a relation that appears only under a flag is one a reader would be
|
|
2844
|
+
// told is absent.
|
|
697
2845
|
includeRelations: true,
|
|
698
2846
|
validateConstraints: true
|
|
699
2847
|
});
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
2848
|
+
spinner.stop();
|
|
2849
|
+
const problem = schemaLoadFailure(analysis.issues, source.schema, "There is nothing to explain.") ?? nothingToGenerate({
|
|
2850
|
+
schema: source.schema,
|
|
2851
|
+
analyzed: analysis.tables,
|
|
2852
|
+
remaining: analysis.tables,
|
|
2853
|
+
consequence: "There is nothing to explain."
|
|
2854
|
+
});
|
|
2855
|
+
if (problem) reportSchemaProblem(out, "explain", problem);
|
|
2856
|
+
const context = { schema: source.label, dialect: analysis.dialect };
|
|
2857
|
+
if (!tableName) {
|
|
2858
|
+
const tables = summarize(analysis);
|
|
2859
|
+
if (out.json) out.jsonData({ command: "explain", exitCode: EXIT_OK, ...context, tables });
|
|
2860
|
+
else out.data(renderIndex(tables, context, out.outStyle));
|
|
2861
|
+
process.exit(EXIT_OK);
|
|
706
2862
|
}
|
|
707
|
-
|
|
2863
|
+
const match = matchTable(analysis.tables, tableName);
|
|
2864
|
+
if (match.kind === "ambiguous") fail(ambiguousTableProblem(tableName, match.hits));
|
|
2865
|
+
if (match.kind === "none") {
|
|
2866
|
+
fail(noSuchTableProblem(tableName, analysis.tables, match.suggestion));
|
|
2867
|
+
}
|
|
2868
|
+
const cfg = source.config;
|
|
2869
|
+
let keptTables;
|
|
2870
|
+
let keptColumns;
|
|
2871
|
+
if (cfg) {
|
|
2872
|
+
keptTables = filterTables(analysis.tables, cfg).map((t) => qualifiedTableName2(t));
|
|
2873
|
+
try {
|
|
2874
|
+
const narrowed = filterColumns(
|
|
2875
|
+
[match.table],
|
|
2876
|
+
cfg.columns
|
|
2877
|
+
);
|
|
2878
|
+
keptColumns = narrowed.tables[0]?.columns.map((c) => c.name);
|
|
2879
|
+
} catch {
|
|
2880
|
+
keptColumns = void 0;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
const explanation = explainTable(
|
|
2884
|
+
analysis,
|
|
2885
|
+
match,
|
|
2886
|
+
{ keptTables, keptColumns }
|
|
2887
|
+
);
|
|
2888
|
+
if (out.json) {
|
|
2889
|
+
out.jsonData({ command: "explain", exitCode: EXIT_OK, ...context, table: explanation });
|
|
2890
|
+
} else {
|
|
2891
|
+
out.data(renderExplanation(explanation, context, out.outStyle));
|
|
2892
|
+
}
|
|
2893
|
+
process.exit(EXIT_OK);
|
|
708
2894
|
} catch (e) {
|
|
709
|
-
const msg =
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
else
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
process.exit(1);
|
|
2895
|
+
const msg = messageOf(e);
|
|
2896
|
+
const code = drzlErrorCode(e, "DRZL_CLI_EXPLAIN");
|
|
2897
|
+
if (opts.json) out.jsonData(jsonFailure("explain", code, msg));
|
|
2898
|
+
else if (e instanceof ConfigValidationError) out.error(msg);
|
|
2899
|
+
else {
|
|
2900
|
+
out.error("Explain failed (DRZL_CLI_EXPLAIN):", msg);
|
|
2901
|
+
out.hint("Tip: run with --json for structured output.");
|
|
2902
|
+
}
|
|
2903
|
+
process.exit(EXIT_FAILED);
|
|
719
2904
|
}
|
|
720
2905
|
});
|
|
721
|
-
|
|
722
|
-
"--
|
|
723
|
-
|
|
2906
|
+
withOutputFlags(
|
|
2907
|
+
program.command("generate").description("Run configured generators (drzl.config.*)").option("-c, --config <path>", "path to drzl.config").option("-s, --schema <path>", "path to the schema, overriding the config").option(
|
|
2908
|
+
"--only <kinds>",
|
|
2909
|
+
`run only these generator kinds, comma separated: ${kindList()}`
|
|
2910
|
+
).option(
|
|
2911
|
+
"--check",
|
|
2912
|
+
"regenerate and fail if the result differs from what is on disk, without changing it"
|
|
2913
|
+
).option("--dry-run", "report what would be written, and write nothing", false)
|
|
724
2914
|
).action(async (opts) => {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
for (const w of [...narrowed.warnings, ...filterWarnings]) console.warn(chalk3.yellow(w));
|
|
747
|
-
reportWideColumns(analysis.issues);
|
|
748
|
-
const driftDirs = computeGeneratorOutputDirs(cfg);
|
|
749
|
-
const driftBefore = opts.check ? await snapshotAll(driftDirs) : null;
|
|
750
|
-
const progress = new cliProgress.SingleBar(
|
|
751
|
-
{ hideCursor: true },
|
|
752
|
-
cliProgress.Presets.shades_classic
|
|
753
|
-
);
|
|
754
|
-
const total = analysis.tables.length || 1;
|
|
755
|
-
progress.start(total, 0);
|
|
756
|
-
const servicesDir = cfg.generators.find((x) => x.kind === "service")?.path ?? "src/services";
|
|
757
|
-
for (const g of cfg.generators) {
|
|
758
|
-
if (g.kind === "orpc") {
|
|
759
|
-
const gen = new ORPCGenerator(analysis);
|
|
760
|
-
const { files } = await gen.generate({
|
|
761
|
-
outputDir: cfg.outDir,
|
|
762
|
-
template: g.template,
|
|
763
|
-
includeRelations: g.includeRelations,
|
|
764
|
-
naming: g.naming,
|
|
765
|
-
outputHeader: g.outputHeader,
|
|
766
|
-
format: g.format,
|
|
767
|
-
templateOptions: g.templateOptions,
|
|
768
|
-
importExtension: g.importExtension,
|
|
769
|
-
validation: g.validation,
|
|
770
|
-
// Documented on this generator since it was added and never reachable from a config
|
|
771
|
-
// file, because the config schema had no such key and zod stripped it in silence.
|
|
772
|
-
databaseInjection: g.databaseInjection,
|
|
773
|
-
servicesDir,
|
|
774
|
-
onProgress: ({ index }) => progress.update(index)
|
|
775
|
-
});
|
|
776
|
-
progress.stop();
|
|
777
|
-
ora().succeed(chalk3.green(`Generated (${g.kind}): ${files.length} files`));
|
|
778
|
-
files.forEach((f) => console.log(" -", chalk3.cyan(f)));
|
|
779
|
-
} else if (g.kind === "trpc") {
|
|
780
|
-
try {
|
|
781
|
-
const { TRPCGenerator } = await loadGenerator(
|
|
782
|
-
"@drzl/generator-trpc",
|
|
783
|
-
() => import("./dist-XBGVORL3.js")
|
|
784
|
-
);
|
|
785
|
-
const gen = new TRPCGenerator(analysis);
|
|
786
|
-
const { files } = await gen.generate({
|
|
787
|
-
...trpcOptions(g, cfg, servicesDir),
|
|
788
|
-
onProgress: ({ index }) => progress.update(index)
|
|
789
|
-
});
|
|
790
|
-
progress.stop();
|
|
791
|
-
ora().succeed(chalk3.green(`Generated (trpc): ${files.length} files`));
|
|
792
|
-
files.forEach((f) => console.log(" -", chalk3.cyan(f)));
|
|
793
|
-
} catch (e) {
|
|
794
|
-
progress.stop();
|
|
795
|
-
reportGeneratorFailure(g.kind, e);
|
|
796
|
-
process.exit(1);
|
|
2915
|
+
const out = outputFor(opts);
|
|
2916
|
+
const planning = !!opts.check || !!opts.dryRun;
|
|
2917
|
+
const emitted = [];
|
|
2918
|
+
const warnings = [];
|
|
2919
|
+
const warn = (text) => {
|
|
2920
|
+
warnings.push(text);
|
|
2921
|
+
out.warn(text);
|
|
2922
|
+
};
|
|
2923
|
+
{
|
|
2924
|
+
try {
|
|
2925
|
+
const only = parseOnly(opts.only);
|
|
2926
|
+
let cfg = await loadConfig(opts.config, warn);
|
|
2927
|
+
if (!cfg && only) {
|
|
2928
|
+
cfg = configFromKinds([...only], opts.schema, warn);
|
|
2929
|
+
}
|
|
2930
|
+
if (!cfg) {
|
|
2931
|
+
const msg = "No config found (DRZL_CFG_001). Create drzl.config.ts or pass --config.";
|
|
2932
|
+
if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_CFG_001", msg));
|
|
2933
|
+
else {
|
|
2934
|
+
out.error(msg);
|
|
2935
|
+
out.hint("Or run one generator with no config: drzl generate --schema <path> --only <kind>.");
|
|
797
2936
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
dbImportPath: g.dbImportPath,
|
|
812
|
-
schemaImportPath: g.schemaImportPath,
|
|
813
|
-
importExtension: g.importExtension,
|
|
814
|
-
// The other half of `databaseInjection`. A router generator in injection mode
|
|
815
|
-
// emits `Service.getById(ctx.db, id)`, and only a service generated in the same
|
|
816
|
-
// mode has a `db` parameter to receive it. This branch never passed the option, so
|
|
817
|
-
// the two halves of one generated project disagreed about the signature.
|
|
818
|
-
databaseInjection: g.databaseInjection
|
|
819
|
-
});
|
|
820
|
-
progress.stop();
|
|
821
|
-
ora().succeed(chalk3.green(`Generated (service): ${files.length} files`));
|
|
822
|
-
files.forEach((f) => console.log(" -", chalk3.cyan(f)));
|
|
823
|
-
} catch (e) {
|
|
824
|
-
progress.stop();
|
|
825
|
-
reportGeneratorFailure(g.kind, e);
|
|
826
|
-
process.exit(1);
|
|
2937
|
+
process.exit(EXIT_FAILED);
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
if (opts.schema) {
|
|
2941
|
+
const { drizzleKit: _fromConfig, ...rest } = cfg;
|
|
2942
|
+
cfg = { ...rest, schema: opts.schema };
|
|
2943
|
+
}
|
|
2944
|
+
const nothingSelected = emptySelectionMessage(only, cfg.generators);
|
|
2945
|
+
if (nothingSelected) {
|
|
2946
|
+
if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_CLI_ONLY", nothingSelected));
|
|
2947
|
+
else {
|
|
2948
|
+
out.error(nothingSelected);
|
|
2949
|
+
out.hint('Add it to "generators" in your config, or name a kind that is already there.');
|
|
827
2950
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
2951
|
+
process.exit(EXIT_FAILED);
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
const source = await resolveSchemaSource(cfg);
|
|
2955
|
+
for (const w of source.warnings) warn(w);
|
|
2956
|
+
if (!cfg.schema && Array.isArray(source.schema) && source.schema.length === 1) {
|
|
2957
|
+
cfg = { ...cfg, schema: source.schema[0] };
|
|
2958
|
+
}
|
|
2959
|
+
if (source.source === "drizzle-kit") {
|
|
2960
|
+
const n = source.schema.length;
|
|
2961
|
+
out.note(
|
|
2962
|
+
out.errStyle.gray(
|
|
2963
|
+
`Schema from ${path7.relative(process.cwd(), source.drizzleKitConfigPath)} (${n} file${n === 1 ? "" : "s"})`
|
|
2964
|
+
)
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
const analyzer = new SchemaAnalyzer2(source.schema);
|
|
2968
|
+
const spinner = out.spinner("Analyzing...");
|
|
2969
|
+
const t0 = Date.now();
|
|
2970
|
+
const analysis = await analyzer.analyze({
|
|
2971
|
+
includeRelations: cfg.analyzer.includeRelations,
|
|
2972
|
+
validateConstraints: cfg.analyzer.validateConstraints,
|
|
2973
|
+
includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
|
|
2974
|
+
});
|
|
2975
|
+
const loadFailure = schemaLoadFailure(analysis.issues, source.schema);
|
|
2976
|
+
if (loadFailure) {
|
|
2977
|
+
spinner.stop();
|
|
2978
|
+
reportSchemaProblem(out, "generate", loadFailure);
|
|
2979
|
+
}
|
|
2980
|
+
spinner.succeed(`Analysis complete in ${Date.now() - t0}ms`);
|
|
2981
|
+
const dialectWarning = dialectMismatchWarning({
|
|
2982
|
+
configPath: source.drizzleKitConfigPath ?? "",
|
|
2983
|
+
declared: source.drizzleKitDialect,
|
|
2984
|
+
analyzed: analysis.dialect
|
|
2985
|
+
});
|
|
2986
|
+
if (dialectWarning) warn(dialectWarning);
|
|
2987
|
+
const narrowed = filterColumns(analysis.tables, cfg.columns);
|
|
2988
|
+
const filterWarnings = tableFilterWarnings(narrowed.tables, cfg);
|
|
2989
|
+
analysis.tables = filterTables(narrowed.tables, cfg);
|
|
2990
|
+
for (const w of [...narrowed.warnings, ...filterWarnings]) warn(w);
|
|
2991
|
+
for (const w of wideColumnWarning(analysis.issues)) warn(w);
|
|
2992
|
+
const empty = nothingToGenerate({
|
|
2993
|
+
schema: source.schema,
|
|
2994
|
+
analyzed: narrowed.tables,
|
|
2995
|
+
remaining: analysis.tables
|
|
2996
|
+
});
|
|
2997
|
+
if (empty) reportSchemaProblem(out, "generate", empty);
|
|
2998
|
+
const outputDirs = computeGeneratorOutputDirs(cfg);
|
|
2999
|
+
const existing = planning ? await snapshotAll(outputDirs) : void 0;
|
|
3000
|
+
const plan = new EmitPlan({ write: !planning, existing });
|
|
3001
|
+
const total = analysis.tables.length || 1;
|
|
3002
|
+
const progress = out.progress(total);
|
|
3003
|
+
const generated = (kind, files) => {
|
|
3004
|
+
progress.stop();
|
|
3005
|
+
const missed = plan.unrecorded(files);
|
|
3006
|
+
if (missed.length && planning) {
|
|
3007
|
+
const message = `The ${kind} generator wrote ${missed.length} file(s) directly instead of reporting them, so this run could not be a preview. Update @drzl/generator-${kind} to a version that supports --dry-run. First file: ${displayPath(missed[0])}`;
|
|
3008
|
+
if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_GEN_003", message));
|
|
3009
|
+
else out.error(message);
|
|
3010
|
+
process.exit(EXIT_FAILED);
|
|
846
3011
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
3012
|
+
const verdicts = plan.verdictsFor(files).filter(Boolean);
|
|
3013
|
+
emitted.push({
|
|
3014
|
+
kind,
|
|
3015
|
+
files,
|
|
3016
|
+
changes: verdicts.map((v) => ({ file: v.file, status: v.verdict }))
|
|
3017
|
+
});
|
|
3018
|
+
if (opts.json) return;
|
|
3019
|
+
if (out.quiet) return;
|
|
3020
|
+
out.succeed(
|
|
3021
|
+
out.errStyle.green(
|
|
3022
|
+
`${planning ? "Would write" : "Generated"} (${kind}): ${files.length} files`
|
|
3023
|
+
) + out.errStyle.gray(` (${describeCounts(plan.counts(files))})`)
|
|
3024
|
+
);
|
|
3025
|
+
for (const v of verdicts) {
|
|
3026
|
+
if (opts.check) break;
|
|
3027
|
+
if (v.verdict === "unchanged") continue;
|
|
3028
|
+
const mark = v.verdict === "created" ? "+" : "~";
|
|
3029
|
+
out.note(" " + out.errStyle.cyan(mark + " " + displayPath(v.file)));
|
|
865
3030
|
}
|
|
866
|
-
|
|
3031
|
+
for (const f of files) out.data(" - " + out.outStyle.cyan(f));
|
|
3032
|
+
};
|
|
3033
|
+
const failGenerator = (kind, e) => {
|
|
3034
|
+
progress.stop();
|
|
3035
|
+
const message = reportGeneratorFailure(out, kind, e);
|
|
3036
|
+
if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_GEN_002", message));
|
|
3037
|
+
process.exit(EXIT_FAILED);
|
|
3038
|
+
};
|
|
3039
|
+
const servicesDir = resolveServicesDir(cfg);
|
|
3040
|
+
for (const g of selectGenerators(cfg.generators, only)) {
|
|
3041
|
+
progress.start();
|
|
3042
|
+
const entry = GENERATOR_BY_KIND.get(g.kind);
|
|
3043
|
+
if (!entry) continue;
|
|
867
3044
|
try {
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
validationOptions(g, cfg, target, { schemaTypes: false })
|
|
876
|
-
);
|
|
877
|
-
progress.stop();
|
|
878
|
-
ora().succeed(chalk3.green(`Generated (arktype): ${files.length} files`));
|
|
879
|
-
files.forEach((f) => console.log(" -", chalk3.cyan(f)));
|
|
3045
|
+
const files = await runGenerator(entry, g, cfg, {
|
|
3046
|
+
analysis,
|
|
3047
|
+
servicesDir,
|
|
3048
|
+
fileSink: plan,
|
|
3049
|
+
onProgress: ({ index }) => progress.update(index)
|
|
3050
|
+
});
|
|
3051
|
+
generated(g.kind, files);
|
|
880
3052
|
} catch (e) {
|
|
881
|
-
|
|
882
|
-
reportGeneratorFailure(g.kind, e);
|
|
883
|
-
process.exit(1);
|
|
3053
|
+
failGenerator(g.kind, e);
|
|
884
3054
|
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
progress.stop();
|
|
899
|
-
reportGeneratorFailure(g.kind, e);
|
|
900
|
-
process.exit(1);
|
|
3055
|
+
}
|
|
3056
|
+
const generatorsDocument = () => emitted.map((e) => ({
|
|
3057
|
+
kind: e.kind,
|
|
3058
|
+
files: e.files,
|
|
3059
|
+
changes: e.changes.map((c) => ({ file: displayPath(c.file), status: c.status }))
|
|
3060
|
+
}));
|
|
3061
|
+
if (planning) {
|
|
3062
|
+
const wrote = await verifyNothingWasWritten(outputDirs, existing);
|
|
3063
|
+
if (wrote.length) {
|
|
3064
|
+
const message = `${wrote.length} file(s) were written by a run that promised to write none, and have been restored. This means an installed generator package is older than this CLI. Update your @drzl/generator-* packages. First file: ${displayPath(wrote[0])}`;
|
|
3065
|
+
if (opts.json) out.jsonData(jsonFailure("generate", "DRZL_GEN_003", message));
|
|
3066
|
+
else out.error(message);
|
|
3067
|
+
process.exit(EXIT_FAILED);
|
|
901
3068
|
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
3069
|
+
}
|
|
3070
|
+
if (opts.check) {
|
|
3071
|
+
const drift = pendingChanges(plan);
|
|
3072
|
+
const upToDate = drift.length === 0;
|
|
3073
|
+
const code = upToDate ? EXIT_OK : EXIT_FINDINGS;
|
|
3074
|
+
if (opts.json) {
|
|
3075
|
+
out.jsonData({
|
|
3076
|
+
ok: true,
|
|
3077
|
+
command: "generate",
|
|
3078
|
+
exitCode: code,
|
|
3079
|
+
check: {
|
|
3080
|
+
upToDate,
|
|
3081
|
+
drift: drift.map((d, i) => ({
|
|
3082
|
+
file: displayPath(d.file),
|
|
3083
|
+
status: driftStatusOf(d.verdict),
|
|
3084
|
+
// Item 81. Beyond the cap the entry is still here with its status, and only the
|
|
3085
|
+
// diff is absent, so a machine reading this never loses a file.
|
|
3086
|
+
diff: i < DIFF_FILE_CAP ? unifiedDiff(d.before ?? "", d.after, {
|
|
3087
|
+
fromLabel: `a/${displayPath(d.file)}`,
|
|
3088
|
+
toLabel: `b/${displayPath(d.file)}`
|
|
3089
|
+
}) : null
|
|
3090
|
+
})),
|
|
3091
|
+
diffFileCap: DIFF_FILE_CAP
|
|
3092
|
+
},
|
|
3093
|
+
generators: generatorsDocument(),
|
|
3094
|
+
warnings
|
|
3095
|
+
});
|
|
3096
|
+
process.exit(code);
|
|
923
3097
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
);
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
files.forEach((f) => console.log(" -", chalk3.cyan(f)));
|
|
938
|
-
} catch (e) {
|
|
939
|
-
progress.stop();
|
|
940
|
-
reportGeneratorFailure(g.kind, e);
|
|
941
|
-
process.exit(1);
|
|
3098
|
+
if (!upToDate) {
|
|
3099
|
+
out.error(`
|
|
3100
|
+
Generated output is out of date (${drift.length} file(s)):`);
|
|
3101
|
+
for (const d of drift) {
|
|
3102
|
+
const status = driftStatusOf(d.verdict);
|
|
3103
|
+
const mark = status === "added" ? "+" : "~";
|
|
3104
|
+
out.error(
|
|
3105
|
+
` ${mark} ${out.errStyle.yellow(status.padEnd(8))} ${displayPath(d.file)}`
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
printCheckDiffs(out, drift);
|
|
3109
|
+
out.hint("\nRun `drzl generate` and commit the result. Nothing was written by this check.");
|
|
3110
|
+
process.exit(code);
|
|
942
3111
|
}
|
|
3112
|
+
out.succeed(out.errStyle.green("Generated output is up to date."));
|
|
3113
|
+
process.exit(code);
|
|
943
3114
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
"\nRun `drzl generate` and commit the result. Nothing was written by this check."
|
|
961
|
-
)
|
|
3115
|
+
if (opts.json) {
|
|
3116
|
+
out.jsonData({
|
|
3117
|
+
ok: true,
|
|
3118
|
+
command: "generate",
|
|
3119
|
+
exitCode: EXIT_OK,
|
|
3120
|
+
check: null,
|
|
3121
|
+
dryRun: !!opts.dryRun,
|
|
3122
|
+
generators: generatorsDocument(),
|
|
3123
|
+
warnings
|
|
3124
|
+
});
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
if (opts.dryRun) {
|
|
3128
|
+
const counts = plan.counts();
|
|
3129
|
+
out.succeed(
|
|
3130
|
+
out.errStyle.green(`Dry run: ${counts.total} file(s) would be written`) + out.errStyle.gray(` (${describeCounts(counts)}). Nothing was written.`)
|
|
962
3131
|
);
|
|
963
|
-
process.exit(
|
|
3132
|
+
process.exit(EXIT_OK);
|
|
964
3133
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
3134
|
+
if (cfg.generators.length) {
|
|
3135
|
+
maybeShowSponsorMessage({ reason: "generate", out });
|
|
3136
|
+
}
|
|
3137
|
+
} catch (e) {
|
|
3138
|
+
const msg = messageOf(e);
|
|
3139
|
+
const code = drzlErrorCode(e, "DRZL_GEN_001");
|
|
3140
|
+
if (e instanceof KindSelectionError) {
|
|
3141
|
+
if (opts.json) out.jsonData(jsonFailure("generate", e.code, msg));
|
|
3142
|
+
else {
|
|
3143
|
+
out.error(msg);
|
|
3144
|
+
if (e.hint) out.hint(e.hint);
|
|
3145
|
+
}
|
|
3146
|
+
process.exit(EXIT_FAILED);
|
|
3147
|
+
}
|
|
3148
|
+
if (opts.json) out.jsonData(jsonFailure("generate", code, msg));
|
|
3149
|
+
else if (e instanceof ConfigValidationError) {
|
|
3150
|
+
out.error(msg);
|
|
3151
|
+
} else {
|
|
3152
|
+
out.error("Generate failed (DRZL_GEN_001):", msg);
|
|
3153
|
+
out.hint("Tip: check your drzl.config.ts and template path.");
|
|
3154
|
+
}
|
|
3155
|
+
process.exit(EXIT_FAILED);
|
|
970
3156
|
}
|
|
971
|
-
} catch (e) {
|
|
972
|
-
console.error(
|
|
973
|
-
chalk3.red("Generate failed (DRZL_GEN_001):"),
|
|
974
|
-
e?.message ?? e,
|
|
975
|
-
"\nTip: check your drzl.config.ts and template path."
|
|
976
|
-
);
|
|
977
|
-
process.exit(1);
|
|
978
3157
|
}
|
|
979
3158
|
});
|
|
980
|
-
|
|
3159
|
+
function schemaProblemFor(analysis, schema) {
|
|
3160
|
+
return schemaLoadFailure(analysis.issues, schema) ?? nothingToGenerate({ schema, analyzed: analysis.tables, remaining: analysis.tables });
|
|
3161
|
+
}
|
|
3162
|
+
function deprecationNotice(command, kind, schema, cmd) {
|
|
3163
|
+
const replacement = `drzl generate --schema ${schema} --only ${kind}`;
|
|
3164
|
+
const CONFIG_KEYS = {
|
|
3165
|
+
outDir: "outDir",
|
|
3166
|
+
template: "template",
|
|
3167
|
+
includeRelations: "includeRelations",
|
|
3168
|
+
servicesDir: "the service generator's path"
|
|
3169
|
+
};
|
|
3170
|
+
const moved = Object.keys(CONFIG_KEYS).filter(
|
|
3171
|
+
(name) => cmd.getOptionValueSource(name) === "cli"
|
|
3172
|
+
);
|
|
3173
|
+
const tail = moved.length ? ` (${moved.map((name) => CONFIG_KEYS[name]).join(", ")} ${moved.length === 1 ? "moves" : "move"} into drzl.config.ts)` : "";
|
|
3174
|
+
return `${command} is deprecated and will be removed in 5.0. Run this instead: ${replacement}${tail}`;
|
|
3175
|
+
}
|
|
3176
|
+
withOutputFlags(
|
|
3177
|
+
program.command("generate:orpc").description("Deprecated. Use `drzl generate --schema <path> --only orpc`").argument("<schema>", "path to drizzle schema (TS)").option("-o, --outDir <dir>", "output directory", "src/api").option("--template <name>", "template name", "standard").option("--includeRelations", "include relation endpoints")
|
|
3178
|
+
).action(async (schema, opts, cmd) => {
|
|
3179
|
+
const out = outputFor(opts);
|
|
3180
|
+
out.warn(deprecationNotice("generate:orpc", "orpc", schema, cmd));
|
|
981
3181
|
try {
|
|
982
|
-
const analyzer = new
|
|
3182
|
+
const analyzer = new SchemaAnalyzer2(schema);
|
|
983
3183
|
const analysis = await analyzer.analyze({
|
|
984
3184
|
includeRelations: !!opts.includeRelations,
|
|
985
3185
|
validateConstraints: true
|
|
986
3186
|
});
|
|
987
|
-
const
|
|
988
|
-
|
|
3187
|
+
const problem = schemaProblemFor(analysis, schema);
|
|
3188
|
+
if (problem) reportSchemaProblem(out, "generate:orpc", problem);
|
|
3189
|
+
const files = await runGeneratorWithOptions(entryFor("orpc"), analysis, {
|
|
989
3190
|
outputDir: opts.outDir,
|
|
990
3191
|
template: opts.template,
|
|
991
3192
|
includeRelations: !!opts.includeRelations
|
|
992
3193
|
});
|
|
993
|
-
|
|
994
|
-
|
|
3194
|
+
if (opts.json) {
|
|
3195
|
+
out.jsonData({
|
|
3196
|
+
ok: true,
|
|
3197
|
+
command: "generate:orpc",
|
|
3198
|
+
exitCode: EXIT_OK,
|
|
3199
|
+
generators: [{ kind: "orpc", files }]
|
|
3200
|
+
});
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
if (!out.quiet) {
|
|
3204
|
+
out.data(out.outStyle.green("Generated:") + " " + files.map((f) => out.outStyle.cyan(f)).join(", "));
|
|
3205
|
+
}
|
|
3206
|
+
maybeShowSponsorMessage({ reason: "generate:orpc", out });
|
|
995
3207
|
} catch (e) {
|
|
996
|
-
|
|
997
|
-
|
|
3208
|
+
let message;
|
|
3209
|
+
if (e instanceof GeneratorNotInstalledError) {
|
|
3210
|
+
message = reportGeneratorFailure(out, "orpc", e);
|
|
3211
|
+
} else {
|
|
3212
|
+
message = messageOf(e);
|
|
3213
|
+
out.error("Generate orpc failed:", message);
|
|
3214
|
+
}
|
|
3215
|
+
if (opts.json) out.jsonData(jsonFailure("generate:orpc", "DRZL_CLI_ORPC", message));
|
|
3216
|
+
process.exit(EXIT_FAILED);
|
|
998
3217
|
}
|
|
999
3218
|
});
|
|
1000
|
-
|
|
3219
|
+
withOutputFlags(
|
|
3220
|
+
program.command("generate:trpc").description("Deprecated. Use `drzl generate --schema <path> --only trpc`").argument("<schema>", "path to drizzle schema (TS)").option("-o, --outDir <dir>", "output directory", "src/api").option("--template <name>", "standard | service", "standard").option("--includeRelations", "include relation endpoints").option("--servicesDir <dir>", "where the service generator writes", "src/services")
|
|
3221
|
+
).action(async (schema, opts, cmd) => {
|
|
3222
|
+
const out = outputFor(opts);
|
|
3223
|
+
out.warn(deprecationNotice("generate:trpc", "trpc", schema, cmd));
|
|
1001
3224
|
try {
|
|
1002
|
-
const analyzer = new
|
|
3225
|
+
const analyzer = new SchemaAnalyzer2(schema);
|
|
1003
3226
|
const analysis = await analyzer.analyze({
|
|
1004
3227
|
includeRelations: !!opts.includeRelations,
|
|
1005
3228
|
validateConstraints: true
|
|
1006
3229
|
});
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
);
|
|
1011
|
-
const gen = new TRPCGenerator(analysis);
|
|
1012
|
-
const { files } = await gen.generate({
|
|
3230
|
+
const problem = schemaProblemFor(analysis, schema);
|
|
3231
|
+
if (problem) reportSchemaProblem(out, "generate:trpc", problem);
|
|
3232
|
+
const files = await runGeneratorWithOptions(entryFor("trpc"), analysis, {
|
|
1013
3233
|
outputDir: opts.outDir,
|
|
1014
3234
|
template: opts.template,
|
|
1015
3235
|
includeRelations: !!opts.includeRelations,
|
|
@@ -1017,27 +3237,95 @@ program.command("generate:trpc").argument("<schema>", "path to drizzle schema (T
|
|
|
1017
3237
|
// cannot become the branch that forgets it.
|
|
1018
3238
|
servicesDir: opts.servicesDir
|
|
1019
3239
|
});
|
|
1020
|
-
|
|
1021
|
-
|
|
3240
|
+
if (opts.json) {
|
|
3241
|
+
out.jsonData({
|
|
3242
|
+
ok: true,
|
|
3243
|
+
command: "generate:trpc",
|
|
3244
|
+
exitCode: EXIT_OK,
|
|
3245
|
+
generators: [{ kind: "trpc", files }]
|
|
3246
|
+
});
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
if (!out.quiet) {
|
|
3250
|
+
out.data(
|
|
3251
|
+
out.outStyle.green("Generated:") + " " + files.map((f) => out.outStyle.cyan(f)).join(", ")
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
maybeShowSponsorMessage({ reason: "generate:trpc", out });
|
|
1022
3255
|
} catch (e) {
|
|
1023
|
-
reportGeneratorFailure("trpc", e);
|
|
1024
|
-
|
|
3256
|
+
const message = reportGeneratorFailure(out, "trpc", e);
|
|
3257
|
+
if (opts.json) out.jsonData(jsonFailure("generate:trpc", "DRZL_CLI_TRPC", message));
|
|
3258
|
+
process.exit(EXIT_FAILED);
|
|
1025
3259
|
}
|
|
1026
3260
|
});
|
|
1027
|
-
program.command("watch").description("Watch schema and regenerate on changes").option("-c, --config <path>", "path to drzl.config").option(
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
3261
|
+
program.command("watch").description("Watch schema and regenerate on changes").option("-c, --config <path>", "path to drzl.config").option(
|
|
3262
|
+
"--only <kinds>",
|
|
3263
|
+
`rebuild only these generator kinds, comma separated: ${kindList()}`
|
|
3264
|
+
).option(
|
|
3265
|
+
"--pipeline <name>",
|
|
3266
|
+
"all | analyze | generate-<kind>, the older spelling of --only",
|
|
3267
|
+
"all"
|
|
3268
|
+
).option("--debounce <ms>", "wait this long after the last change before rebuilding", "200").option("--clear", "clear the terminal before each rebuild", false).option("--json", "emit JSON logs", false).option("-q, --quiet", "drop the progress narration on stderr; errors still print", false).option("--poll", "force polling (helps WSL/Docker/remote FS)", false).action(async (opts) => {
|
|
3269
|
+
const out = outputFor(opts);
|
|
3270
|
+
let selection;
|
|
3271
|
+
try {
|
|
3272
|
+
selection = resolveWatchSelection(opts);
|
|
3273
|
+
} catch (e) {
|
|
3274
|
+
if (e instanceof KindSelectionError) {
|
|
3275
|
+
if (opts.json) out.jsonData(jsonFailure("watch", e.code, e.message));
|
|
3276
|
+
else {
|
|
3277
|
+
out.error(e.message);
|
|
3278
|
+
if (e.hint) out.hint(e.hint);
|
|
3279
|
+
}
|
|
3280
|
+
} else out.error(messageOf(e));
|
|
3281
|
+
process.exit(EXIT_FAILED);
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
const reportWatchProblem = (problem) => {
|
|
3285
|
+
if (opts.json) {
|
|
3286
|
+
out.jsonData({ event: "error", code: problem.code, message: problem.message });
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
out.error(problem.message);
|
|
3290
|
+
out.hint(problem.hint);
|
|
3291
|
+
};
|
|
3292
|
+
const clearScreen = () => {
|
|
3293
|
+
if (!opts.clear || opts.json || out.quiet) return;
|
|
3294
|
+
if (!out.stderr.isTTY) return;
|
|
3295
|
+
out.stderr.write("\x1B[2J\x1B[3J\x1B[H");
|
|
3296
|
+
};
|
|
3297
|
+
let loaded;
|
|
3298
|
+
try {
|
|
3299
|
+
loaded = await loadConfig(opts.config, (w) => out.warn(w));
|
|
3300
|
+
} catch (e) {
|
|
3301
|
+
out.error(messageOf(e));
|
|
3302
|
+
process.exit(EXIT_FAILED);
|
|
1032
3303
|
return;
|
|
1033
3304
|
}
|
|
1034
|
-
|
|
3305
|
+
if (!loaded) {
|
|
3306
|
+
out.error("No config found (DRZL_CFG_001). Create drzl.config.ts or pass --config.");
|
|
3307
|
+
process.exit(EXIT_FAILED);
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
let cfg = loaded;
|
|
3311
|
+
const abs = (p) => path7.resolve(process.cwd(), p);
|
|
1035
3312
|
const isInside = (child, parent) => {
|
|
1036
|
-
const rel =
|
|
1037
|
-
return !!rel && !rel.startsWith("..") && !
|
|
3313
|
+
const rel = path7.relative(parent, child);
|
|
3314
|
+
return !!rel && !rel.startsWith("..") && !path7.isAbsolute(rel);
|
|
1038
3315
|
};
|
|
3316
|
+
let source;
|
|
3317
|
+
try {
|
|
3318
|
+
source = await resolveSchemaSource(cfg);
|
|
3319
|
+
} catch (e) {
|
|
3320
|
+
out.error(messageOf(e));
|
|
3321
|
+
process.exit(EXIT_FAILED);
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
for (const w of source.warnings) out.warn(w);
|
|
1039
3325
|
const ignoredOutDirs = new Set(computeGeneratorOutputDirs(cfg).map(abs));
|
|
1040
|
-
const currentTargets = new Set(
|
|
3326
|
+
const currentTargets = new Set(
|
|
3327
|
+
computeWatchTargets(cfg, process.cwd(), source).map(abs)
|
|
3328
|
+
);
|
|
1041
3329
|
const syncWatcherTargets = (watcher2, next) => {
|
|
1042
3330
|
const add = [];
|
|
1043
3331
|
const del = [];
|
|
@@ -1059,7 +3347,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
1059
3347
|
if (full === dir || isInside(full, dir)) return true;
|
|
1060
3348
|
}
|
|
1061
3349
|
if (stats?.isDirectory()) return false;
|
|
1062
|
-
const ext =
|
|
3350
|
+
const ext = path7.extname(full);
|
|
1063
3351
|
if (!ext) return false;
|
|
1064
3352
|
return !WATCHED_EXTENSIONS.has(ext);
|
|
1065
3353
|
};
|
|
@@ -1070,7 +3358,7 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
1070
3358
|
ignored: ignoredFn
|
|
1071
3359
|
});
|
|
1072
3360
|
const logTrigger = (type, file) => {
|
|
1073
|
-
if (opts.json)
|
|
3361
|
+
if (opts.json) out.jsonData({ event: "trigger", type, file });
|
|
1074
3362
|
};
|
|
1075
3363
|
watcher.on("add", (p) => {
|
|
1076
3364
|
logTrigger("add", p);
|
|
@@ -1083,264 +3371,127 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
1083
3371
|
trigger(p);
|
|
1084
3372
|
});
|
|
1085
3373
|
let lastFiles = [];
|
|
3374
|
+
const watchGenerated = (kind, files) => {
|
|
3375
|
+
if (opts.json) {
|
|
3376
|
+
out.jsonData({ event: "generate_complete", kind, files });
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
out.succeed(
|
|
3380
|
+
out.errStyle.green(`Generated (${kind}): ${files.length} files`) + (files.length ? " " + files.map((f) => out.errStyle.cyan(f)).join(", ") : "")
|
|
3381
|
+
);
|
|
3382
|
+
};
|
|
1086
3383
|
const run = async () => {
|
|
1087
3384
|
try {
|
|
1088
|
-
const reloaded = await loadConfig(opts.config);
|
|
3385
|
+
const reloaded = await loadConfig(opts.config, (w) => out.warn(w));
|
|
1089
3386
|
if (!reloaded) throw new Error("Config disappeared during watch.");
|
|
1090
3387
|
cfg = reloaded;
|
|
3388
|
+
source = await resolveSchemaSource(cfg);
|
|
3389
|
+
if (!cfg.schema && Array.isArray(source.schema) && source.schema.length === 1) {
|
|
3390
|
+
cfg = { ...cfg, schema: source.schema[0] };
|
|
3391
|
+
}
|
|
1091
3392
|
rebuildIgnoreDirsFrom(cfg);
|
|
1092
|
-
const nextTargets = new Set(
|
|
3393
|
+
const nextTargets = new Set(
|
|
3394
|
+
computeWatchTargets(cfg, process.cwd(), source).map(abs)
|
|
3395
|
+
);
|
|
1093
3396
|
syncWatcherTargets(watcher, nextTargets);
|
|
1094
|
-
|
|
3397
|
+
clearScreen();
|
|
1095
3398
|
if (opts.json) {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
})
|
|
1102
|
-
);
|
|
3399
|
+
out.jsonData({
|
|
3400
|
+
event: "watch_config_applied",
|
|
3401
|
+
targets: Array.from(currentTargets),
|
|
3402
|
+
ignored: Array.from(ignoredOutDirs)
|
|
3403
|
+
});
|
|
1103
3404
|
}
|
|
1104
|
-
const
|
|
3405
|
+
for (const w of source.warnings) out.warn(w);
|
|
3406
|
+
const analyzer = new SchemaAnalyzer2(source.schema);
|
|
1105
3407
|
const analysis = await analyzer.analyze({
|
|
1106
3408
|
includeRelations: cfg.analyzer.includeRelations,
|
|
1107
3409
|
validateConstraints: cfg.analyzer.validateConstraints,
|
|
1108
3410
|
includeHeuristicRelations: cfg.analyzer.includeHeuristicRelations
|
|
1109
3411
|
});
|
|
3412
|
+
const dialectWarning = dialectMismatchWarning({
|
|
3413
|
+
configPath: source.drizzleKitConfigPath ?? "",
|
|
3414
|
+
declared: source.drizzleKitDialect,
|
|
3415
|
+
analyzed: analysis.dialect
|
|
3416
|
+
});
|
|
3417
|
+
if (dialectWarning) out.warn(dialectWarning);
|
|
3418
|
+
const loadFailure = schemaLoadFailure(analysis.issues, source.schema);
|
|
3419
|
+
if (loadFailure) {
|
|
3420
|
+
reportWatchProblem(loadFailure);
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
1110
3423
|
const narrowed = filterColumns(analysis.tables, cfg.columns);
|
|
1111
3424
|
const filterWarnings = tableFilterWarnings(narrowed.tables, cfg);
|
|
1112
3425
|
analysis.tables = filterTables(narrowed.tables, cfg);
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
if (
|
|
1116
|
-
if (opts.pipeline === "analyze") {
|
|
3426
|
+
for (const w of [...narrowed.warnings, ...filterWarnings]) out.warn(w);
|
|
3427
|
+
for (const w of wideColumnWarning(analysis.issues)) out.warn(w);
|
|
3428
|
+
if (selection.analyzeOnly) {
|
|
1117
3429
|
if (opts.json) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
})
|
|
1124
|
-
);
|
|
3430
|
+
out.jsonData({
|
|
3431
|
+
event: "analyze_complete",
|
|
3432
|
+
issues: analysis.issues,
|
|
3433
|
+
tables: analysis.tables.length
|
|
3434
|
+
});
|
|
1125
3435
|
} else {
|
|
1126
|
-
|
|
3436
|
+
out.succeed("Analyze complete.");
|
|
1127
3437
|
}
|
|
1128
3438
|
return;
|
|
1129
3439
|
}
|
|
3440
|
+
const empty = nothingToGenerate({
|
|
3441
|
+
schema: source.schema,
|
|
3442
|
+
analyzed: narrowed.tables,
|
|
3443
|
+
remaining: analysis.tables
|
|
3444
|
+
});
|
|
3445
|
+
if (empty) {
|
|
3446
|
+
reportWatchProblem(empty);
|
|
3447
|
+
return;
|
|
3448
|
+
}
|
|
1130
3449
|
const newFiles = [];
|
|
1131
|
-
const servicesDir = cfg
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
"
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
continue;
|
|
3450
|
+
const servicesDir = resolveServicesDir(cfg);
|
|
3451
|
+
const unmatched = emptySelectionMessage(selection.kinds, cfg.generators);
|
|
3452
|
+
if (unmatched) {
|
|
3453
|
+
if (opts.json) out.jsonData({ event: "error", code: "DRZL_CLI_ONLY", message: unmatched });
|
|
3454
|
+
else {
|
|
3455
|
+
out.error(unmatched);
|
|
3456
|
+
out.hint('Add it to "generators" in your config, or name a kind that is already there.');
|
|
1139
3457
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
format: g.format,
|
|
1149
|
-
templateOptions: g.templateOptions,
|
|
1150
|
-
importExtension: g.importExtension,
|
|
1151
|
-
validation: g.validation,
|
|
1152
|
-
databaseInjection: g.databaseInjection,
|
|
1153
|
-
servicesDir
|
|
1154
|
-
});
|
|
1155
|
-
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
1156
|
-
chalk3.green(`Generated (${g.kind}):`),
|
|
1157
|
-
files.map((f) => chalk3.cyan(f)).join(", ")
|
|
1158
|
-
);
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
for (const g of selectGenerators(cfg.generators, selection.kinds)) {
|
|
3461
|
+
const entry = GENERATOR_BY_KIND.get(g.kind);
|
|
3462
|
+
if (!entry) continue;
|
|
3463
|
+
try {
|
|
3464
|
+
const files = await runGenerator(entry, g, cfg, { analysis, servicesDir });
|
|
3465
|
+
watchGenerated(g.kind, files);
|
|
1159
3466
|
newFiles.push(...files);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
"@drzl/generator-trpc",
|
|
1164
|
-
() => import("./dist-XBGVORL3.js")
|
|
1165
|
-
);
|
|
1166
|
-
const gen = new TRPCGenerator(analysis);
|
|
1167
|
-
const { files } = await gen.generate(trpcOptions(g, cfg, servicesDir));
|
|
1168
|
-
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
1169
|
-
chalk3.green(`Generated (trpc): ${files.length} files`),
|
|
1170
|
-
files.map((f) => chalk3.cyan(f)).join(", ")
|
|
1171
|
-
);
|
|
1172
|
-
newFiles.push(...files);
|
|
1173
|
-
} catch (e) {
|
|
1174
|
-
reportGeneratorFailure(g.kind, e);
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
} else if (g.kind === "service") {
|
|
1178
|
-
try {
|
|
1179
|
-
const { ServiceGenerator } = await loadGenerator(
|
|
1180
|
-
"@drzl/generator-service",
|
|
1181
|
-
() => import("@drzl/generator-service")
|
|
1182
|
-
);
|
|
1183
|
-
const gen = new ServiceGenerator(analysis);
|
|
1184
|
-
const target = g.path ?? "src/services";
|
|
1185
|
-
const files = await gen.generate({
|
|
1186
|
-
outDir: target,
|
|
1187
|
-
outputHeader: g.outputHeader,
|
|
1188
|
-
format: g.format,
|
|
1189
|
-
dataAccess: g.dataAccess,
|
|
1190
|
-
dbImportPath: g.dbImportPath,
|
|
1191
|
-
schemaImportPath: g.schemaImportPath,
|
|
1192
|
-
importExtension: g.importExtension,
|
|
1193
|
-
databaseInjection: g.databaseInjection
|
|
1194
|
-
});
|
|
1195
|
-
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
1196
|
-
chalk3.green(`Generated (service): ${files.length} files`),
|
|
1197
|
-
files.map((f) => chalk3.cyan(f)).join(", ")
|
|
1198
|
-
);
|
|
1199
|
-
newFiles.push(...files);
|
|
1200
|
-
} catch (e) {
|
|
1201
|
-
reportGeneratorFailure(g.kind, e);
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
} else if (g.kind === "zod") {
|
|
1205
|
-
try {
|
|
1206
|
-
const { ZodGenerator } = await loadGenerator(
|
|
1207
|
-
"@drzl/generator-zod",
|
|
1208
|
-
() => import("@drzl/generator-zod")
|
|
1209
|
-
);
|
|
1210
|
-
const gen = new ZodGenerator(analysis);
|
|
1211
|
-
const target = g.path ?? "src/validators/zod";
|
|
1212
|
-
const files = await gen.generate(
|
|
1213
|
-
validationOptions(g, cfg, target, { schemaTypes: true, meta: true })
|
|
1214
|
-
);
|
|
1215
|
-
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
1216
|
-
chalk3.green(`Generated (zod): ${files.length} files`),
|
|
1217
|
-
files.map((f) => chalk3.cyan(f)).join(", ")
|
|
1218
|
-
);
|
|
1219
|
-
newFiles.push(...files);
|
|
1220
|
-
} catch (e) {
|
|
1221
|
-
reportGeneratorFailure(g.kind, e);
|
|
1222
|
-
return;
|
|
1223
|
-
}
|
|
1224
|
-
} else if (g.kind === "valibot") {
|
|
1225
|
-
try {
|
|
1226
|
-
const { ValibotGenerator } = await loadGenerator(
|
|
1227
|
-
"@drzl/generator-valibot",
|
|
1228
|
-
() => import("@drzl/generator-valibot")
|
|
1229
|
-
);
|
|
1230
|
-
const gen = new ValibotGenerator(analysis);
|
|
1231
|
-
const target = g.path ?? "src/validators/valibot";
|
|
1232
|
-
const files = await gen.generate(
|
|
1233
|
-
validationOptions(g, cfg, target, { schemaTypes: true })
|
|
1234
|
-
);
|
|
1235
|
-
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
1236
|
-
chalk3.green(`Generated (valibot): ${files.length} files`),
|
|
1237
|
-
files.map((f) => chalk3.cyan(f)).join(", ")
|
|
1238
|
-
);
|
|
1239
|
-
newFiles.push(...files);
|
|
1240
|
-
} catch (e) {
|
|
1241
|
-
reportGeneratorFailure(g.kind, e);
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
} else if (g.kind === "arktype") {
|
|
1245
|
-
try {
|
|
1246
|
-
const { ArkTypeGenerator } = await loadGenerator(
|
|
1247
|
-
"@drzl/generator-arktype",
|
|
1248
|
-
() => import("@drzl/generator-arktype")
|
|
1249
|
-
);
|
|
1250
|
-
const gen = new ArkTypeGenerator(analysis);
|
|
1251
|
-
const target = g.path ?? "src/validators/arktype";
|
|
1252
|
-
const files = await gen.generate(
|
|
1253
|
-
validationOptions(g, cfg, target, { schemaTypes: false })
|
|
1254
|
-
);
|
|
1255
|
-
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
1256
|
-
chalk3.green(`Generated (arktype): ${files.length} files`),
|
|
1257
|
-
files.map((f) => chalk3.cyan(f)).join(", ")
|
|
1258
|
-
);
|
|
1259
|
-
newFiles.push(...files);
|
|
1260
|
-
} catch (e) {
|
|
1261
|
-
reportGeneratorFailure(g.kind, e);
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
} else if (g.kind === "typebox") {
|
|
1265
|
-
try {
|
|
1266
|
-
const { TypeBoxGenerator } = await loadGenerator(
|
|
1267
|
-
"@drzl/generator-typebox",
|
|
1268
|
-
() => import("@drzl/generator-typebox")
|
|
1269
|
-
);
|
|
1270
|
-
const gen = new TypeBoxGenerator(analysis);
|
|
1271
|
-
const target = g.path ?? "src/validators/typebox";
|
|
1272
|
-
const files = await gen.generate(
|
|
1273
|
-
validationOptions(g, cfg, target, {
|
|
1274
|
-
schemaTypes: true,
|
|
1275
|
-
standardSchema: true
|
|
1276
|
-
})
|
|
1277
|
-
);
|
|
1278
|
-
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
1279
|
-
chalk3.green(`Generated (typebox): ${files.length} files`),
|
|
1280
|
-
files.map((f) => chalk3.cyan(f)).join(", ")
|
|
1281
|
-
);
|
|
1282
|
-
newFiles.push(...files);
|
|
1283
|
-
} catch (e) {
|
|
1284
|
-
reportGeneratorFailure(g.kind, e);
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
} else if (g.kind === "effect") {
|
|
1288
|
-
try {
|
|
1289
|
-
const { EffectGenerator } = await loadGenerator(
|
|
1290
|
-
"@drzl/generator-effect",
|
|
1291
|
-
() => import("./dist-WHAW3AMQ.js")
|
|
1292
|
-
);
|
|
1293
|
-
const gen = new EffectGenerator(analysis);
|
|
1294
|
-
const target = g.path ?? "src/validators/effect";
|
|
1295
|
-
const files = await gen.generate(
|
|
1296
|
-
validationOptions(g, cfg, target, { schemaTypes: true })
|
|
1297
|
-
);
|
|
1298
|
-
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
1299
|
-
chalk3.green(`Generated (effect): ${files.length} files`),
|
|
1300
|
-
files.map((f) => chalk3.cyan(f)).join(", ")
|
|
1301
|
-
);
|
|
1302
|
-
newFiles.push(...files);
|
|
1303
|
-
} catch (e) {
|
|
1304
|
-
reportGeneratorFailure(g.kind, e);
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
} else if (g.kind === "json-schema") {
|
|
1308
|
-
try {
|
|
1309
|
-
const { JsonSchemaGenerator } = await loadGenerator(
|
|
1310
|
-
"@drzl/generator-json-schema",
|
|
1311
|
-
() => import("./dist-K6N4F3XW.js")
|
|
1312
|
-
);
|
|
1313
|
-
const gen = new JsonSchemaGenerator(analysis);
|
|
1314
|
-
const target = g.path ?? "src/validators/json-schema";
|
|
1315
|
-
const files = await gen.generate(jsonSchemaOptions(g, cfg, target));
|
|
1316
|
-
opts.json ? console.log(JSON.stringify({ event: "generate_complete", kind: g.kind, files })) : console.log(
|
|
1317
|
-
chalk3.green(`Generated (json-schema): ${files.length} files`),
|
|
1318
|
-
files.map((f) => chalk3.cyan(f)).join(", ")
|
|
1319
|
-
);
|
|
1320
|
-
newFiles.push(...files);
|
|
1321
|
-
} catch (e) {
|
|
1322
|
-
reportGeneratorFailure(g.kind, e);
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
3467
|
+
} catch (e) {
|
|
3468
|
+
reportGeneratorFailure(out, g.kind, e);
|
|
3469
|
+
return;
|
|
1325
3470
|
}
|
|
1326
3471
|
}
|
|
1327
3472
|
const added = newFiles.filter((f) => !lastFiles.includes(f));
|
|
1328
3473
|
const removed = lastFiles.filter((f) => !newFiles.includes(f));
|
|
1329
|
-
opts.json
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
3474
|
+
if (opts.json) {
|
|
3475
|
+
out.jsonData({ event: "diff", added, removed });
|
|
3476
|
+
} else {
|
|
3477
|
+
if (added.length) out.note(out.errStyle.blue(`Added: ${added.join(", ")}`));
|
|
3478
|
+
if (removed.length) out.warn(`Removed: ${removed.join(", ")}`);
|
|
3479
|
+
}
|
|
3480
|
+
if (newFiles.length) {
|
|
3481
|
+
const reason = selection.kinds ? `watch:${[...selection.kinds].join(",")}` : "watch";
|
|
3482
|
+
maybeShowSponsorMessage({ reason, out });
|
|
1336
3483
|
}
|
|
1337
3484
|
lastFiles = newFiles;
|
|
1338
3485
|
} catch (e) {
|
|
1339
|
-
|
|
3486
|
+
const msg = messageOf(e);
|
|
3487
|
+
if (opts.json) out.jsonData({ event: "error", message: msg });
|
|
3488
|
+
else out.error("Watch pipeline failed:", msg);
|
|
1340
3489
|
}
|
|
1341
3490
|
};
|
|
1342
|
-
const
|
|
1343
|
-
|
|
3491
|
+
const scheduler = createRebuildScheduler({
|
|
3492
|
+
run,
|
|
3493
|
+
debounceMs: resolveDebounce(opts.debounce, (w) => out.warn(w))
|
|
3494
|
+
});
|
|
1344
3495
|
const trigger = (file) => {
|
|
1345
3496
|
if (file) {
|
|
1346
3497
|
const full = abs(file);
|
|
@@ -1348,62 +3499,73 @@ program.command("watch").description("Watch schema and regenerate on changes").o
|
|
|
1348
3499
|
if (full === dir || isInside(full, dir)) return;
|
|
1349
3500
|
}
|
|
1350
3501
|
}
|
|
1351
|
-
|
|
1352
|
-
timer = setTimeout(run, debounced);
|
|
3502
|
+
scheduler.trigger();
|
|
1353
3503
|
};
|
|
1354
3504
|
if (opts.json) {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
})
|
|
1361
|
-
);
|
|
3505
|
+
out.jsonData({
|
|
3506
|
+
event: "watching",
|
|
3507
|
+
targets: Array.from(currentTargets),
|
|
3508
|
+
ignored: Array.from(ignoredOutDirs)
|
|
3509
|
+
});
|
|
1362
3510
|
} else {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
"Watching:\n " + Array.from(currentTargets).map((p) =>
|
|
3511
|
+
out.note(
|
|
3512
|
+
out.errStyle.gray(
|
|
3513
|
+
"Watching:\n " + Array.from(currentTargets).map((p) => path7.relative(process.cwd(), p)).join("\n ")
|
|
1366
3514
|
)
|
|
1367
3515
|
);
|
|
1368
3516
|
}
|
|
1369
|
-
watcher.on("add", (p) => trigger(p)).on("change", (p) => trigger(p)).on("unlink", (p) => trigger(p)).on("error", (err) =>
|
|
1370
|
-
await
|
|
3517
|
+
watcher.on("add", (p) => trigger(p)).on("change", (p) => trigger(p)).on("unlink", (p) => trigger(p)).on("error", (err) => out.error("Watcher error:", messageOf(err)));
|
|
3518
|
+
await scheduler.runNow();
|
|
1371
3519
|
});
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
3520
|
+
withOutputFlags(
|
|
3521
|
+
program.command("init").description("Scaffold a drzl.config.ts, finding your schema and asking what to generate").option("-y, --yes", "take the defaults and ask nothing").option("--schema <path>", "the schema file to write into the config, skipping detection").option(
|
|
3522
|
+
"--generators <list>",
|
|
3523
|
+
`comma-separated: ${INIT_GENERATOR_CHOICES.map((c) => c.kind).join(", ")}`
|
|
3524
|
+
)
|
|
3525
|
+
).action(async (opts) => {
|
|
3526
|
+
const out = outputFor(opts);
|
|
3527
|
+
const failures = [];
|
|
3528
|
+
const outcome = await runInit({
|
|
3529
|
+
cwd: process.cwd(),
|
|
3530
|
+
yes: !!opts.yes || !!opts.json,
|
|
3531
|
+
schemaFlag: opts.schema,
|
|
3532
|
+
generatorsFlag: opts.generators,
|
|
3533
|
+
stdin: process.stdin,
|
|
3534
|
+
stdout: process.stdout,
|
|
3535
|
+
env: process.env,
|
|
3536
|
+
// Narration on stderr, all of it: what `init` produces is a file on disk, and the lines it
|
|
3537
|
+
// prints are a report about that.
|
|
3538
|
+
log: (s) => out.note(s.startsWith("Created ") ? out.errStyle.green(s) : out.errStyle.gray(s)),
|
|
3539
|
+
error: (s) => {
|
|
3540
|
+
failures.push(s);
|
|
3541
|
+
out.error(s);
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
3544
|
+
if (opts.json) {
|
|
3545
|
+
out.jsonData(
|
|
3546
|
+
outcome.code === 0 ? {
|
|
3547
|
+
ok: true,
|
|
3548
|
+
command: "init",
|
|
3549
|
+
exitCode: EXIT_OK,
|
|
3550
|
+
written: outcome.written,
|
|
3551
|
+
schema: outcome.plan?.schema ?? null,
|
|
3552
|
+
schemaSource: outcome.plan?.schemaSource ?? null,
|
|
3553
|
+
generators: outcome.plan?.generators ?? []
|
|
3554
|
+
} : jsonFailure("init", "DRZL_CLI_INIT", failures.join(" ") || "init did not write a config")
|
|
3555
|
+
);
|
|
1393
3556
|
}
|
|
3557
|
+
process.exit(outcome.code === 0 ? EXIT_OK : EXIT_FAILED);
|
|
1394
3558
|
});
|
|
1395
|
-
function
|
|
3559
|
+
function wideColumnWarning(issues) {
|
|
1396
3560
|
const wide = issues.filter((i) => i.code === "DRZL_ANL_UNKNOWN_COLUMN");
|
|
1397
|
-
if (!wide.length) return;
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
);
|
|
1402
|
-
for (const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
for (const h of hints) console.warn(chalk3.gray(` ${h}`));
|
|
1406
|
-
console.warn(chalk3.gray(" Run `drzl doctor` for the full report."));
|
|
3561
|
+
if (!wide.length) return [];
|
|
3562
|
+
const lines = [`
|
|
3563
|
+
${wide.length} column${wide.length === 1 ? "" : "s"} could not be typed:`];
|
|
3564
|
+
for (const i of wide.slice(0, 10)) lines.push(` - ${i.message}`);
|
|
3565
|
+
if (wide.length > 10) lines.push(` ... and ${wide.length - 10} more`);
|
|
3566
|
+
for (const h of [...new Set(wide.map((i) => i.hint).filter(Boolean))]) lines.push(` ${h}`);
|
|
3567
|
+
lines.push(" Run `drzl doctor` for the full report.");
|
|
3568
|
+
return [lines.join("\n")];
|
|
1407
3569
|
}
|
|
1408
3570
|
program.parseAsync(process.argv);
|
|
1409
3571
|
//# sourceMappingURL=cli.js.map
|