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