@drzl/validation-core 1.1.0 → 2.1.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/README.md CHANGED
@@ -37,3 +37,23 @@ Shared interfaces and helpers used by validation generators.
37
37
  - Helpers
38
38
  - `insertColumns(table)`, `updateColumns(table)`, `selectColumns(table)`
39
39
  - `formatCode(code, filePath, formatOpts)`
40
+ - File names (shared by every generator that writes a file and a barrel)
41
+ - `moduleFileName(tsName, fileSuffix)` -> `users.zod.ts`
42
+ - `moduleSpecifier(tsName, fileSuffix, importExtension?)` -> `./users.zod.js`, the
43
+ specifier a sibling module needs to import that file.
44
+ - `importSpecifier(relativePath, importExtension?)` does the same for a path a generator
45
+ already has in hand, e.g. `./types/users.ts` -> `./types/users.js`.
46
+ - Both read the same `fileSuffix`, so a barrel can never name a file that was not written.
47
+ - `ImportExtension` is `'js' | 'none' | 'ts'`, defaulting to `DEFAULT_IMPORT_EXTENSION`
48
+ (`'js'`). `'js'` is the only form that resolves under `bundler`, `node10`, `node16` and
49
+ `nodenext`, in both CommonJS and ESM, with no compiler flag. `'none'` is the pre-2.0
50
+ output and misses `node16`/`nodenext` ES modules. `'ts'` needs
51
+ `allowImportingTsExtensions`. `.mts` and `.cts` become `.mjs` and `.cjs` under both
52
+ `'js'` and `'none'`, since an extensionless specifier never resolves to them.
53
+ - Naming (shared by every generator that emits a schema name)
54
+ - `resolveAffix({ affix, schemaSuffix })` -> `ResolvedAffix`
55
+ - `schemaName(mode, tsName, resolved)`, `typeName(mode, tsName, resolved)`
56
+ - `validateAffix(affix, schemaSuffix)` -> issues for unusable or colliding names
57
+ - `pascalCase(s)`, `applyTableCase(tsName, 'preserve' | 'pascal')`
58
+ - Calling `resolveAffix()` with no options reproduces the original naming exactly, so the
59
+ generators and the oRPC router can never interpret one config two ways.
package/dist/index.cjs CHANGED
@@ -100171,13 +100171,197 @@ ${codeblock}`, options8);
100171
100171
  // src/index.ts
100172
100172
  var index_exports2 = {};
100173
100173
  __export(index_exports2, {
100174
+ AFFIX_PROBE_TABLE: () => AFFIX_PROBE_TABLE,
100175
+ DEFAULT_IMPORT_EXTENSION: () => DEFAULT_IMPORT_EXTENSION,
100176
+ DEFAULT_MODE_PREFIX: () => DEFAULT_MODE_PREFIX,
100177
+ DEFAULT_SCHEMA_SUFFIX: () => DEFAULT_SCHEMA_SUFFIX,
100178
+ DEFAULT_TYPE_SUFFIX: () => DEFAULT_TYPE_SUFFIX,
100179
+ IMPORT_EXTENSIONS: () => IMPORT_EXTENSIONS,
100180
+ NAME_MODES: () => NAME_MODES,
100181
+ applyTableCase: () => applyTableCase,
100174
100182
  formatCode: () => formatCode,
100183
+ importSpecifier: () => importSpecifier,
100175
100184
  insertColumns: () => insertColumns,
100176
100185
  isGeneratedColumn: () => isGeneratedColumn,
100186
+ moduleFileName: () => moduleFileName,
100187
+ moduleSpecifier: () => moduleSpecifier,
100188
+ pascalCase: () => pascalCase,
100189
+ resolveAffix: () => resolveAffix,
100190
+ resolveConfiguredImport: () => resolveConfiguredImport,
100191
+ schemaName: () => schemaName,
100177
100192
  selectColumns: () => selectColumns,
100178
- updateColumns: () => updateColumns
100193
+ typeName: () => typeName,
100194
+ updateColumns: () => updateColumns,
100195
+ validateAffix: () => validateAffix
100179
100196
  });
100180
100197
  module.exports = __toCommonJS(index_exports2);
100198
+
100199
+ // src/files.ts
100200
+ var import_node_fs = __toESM(require("fs"), 1);
100201
+ var import_node_path = __toESM(require("path"), 1);
100202
+ var IMPORT_EXTENSIONS = ["js", "none", "ts"];
100203
+ var DEFAULT_IMPORT_EXTENSION = "js";
100204
+ var TS_EXTENSIONS = [
100205
+ { ext: ".mts", js: ".mjs", none: ".mjs" },
100206
+ { ext: ".cts", js: ".cjs", none: ".cjs" },
100207
+ { ext: ".tsx", js: ".js", none: "" },
100208
+ { ext: ".ts", js: ".js", none: "" }
100209
+ ];
100210
+ function moduleFileName(tsName, fileSuffix) {
100211
+ return `${tsName}${fileSuffix}`;
100212
+ }
100213
+ function importSpecifier(relativePath, importExtension = DEFAULT_IMPORT_EXTENSION) {
100214
+ for (const { ext, js: js8, none } of TS_EXTENSIONS) {
100215
+ if (!relativePath.endsWith(ext)) continue;
100216
+ const stem = relativePath.slice(0, -ext.length);
100217
+ if (importExtension === "ts") return relativePath;
100218
+ return `${stem}${importExtension === "none" ? none : js8}`;
100219
+ }
100220
+ return relativePath;
100221
+ }
100222
+ function moduleSpecifier(tsName, fileSuffix, importExtension = DEFAULT_IMPORT_EXTENSION) {
100223
+ return importSpecifier(`./${moduleFileName(tsName, fileSuffix)}`, importExtension);
100224
+ }
100225
+ function resolveConfiguredImport(configured, outDirAbs, cwd, importExtension = DEFAULT_IMPORT_EXTENSION) {
100226
+ if (isPackageSpecifier(configured)) return configured;
100227
+ const targetAbs = import_node_path.default.isAbsolute(configured) ? configured : import_node_path.default.resolve(configured.startsWith(".") ? outDirAbs : cwd, configured);
100228
+ const withIndex = pointsAtDirectory(targetAbs) ? `${configured}/index` : configured;
100229
+ if (configured.startsWith(".")) {
100230
+ return importSpecifier(withTsExtension(withIndex), importExtension);
100231
+ }
100232
+ const resolved = pointsAtDirectory(targetAbs) ? import_node_path.default.join(targetAbs, "index") : targetAbs;
100233
+ const rel = import_node_path.default.relative(outDirAbs, resolved).split(import_node_path.default.sep).join("/");
100234
+ const prefixed = !rel ? "." : rel.startsWith(".") ? rel : `./${rel}`;
100235
+ return importSpecifier(withTsExtension(prefixed), importExtension);
100236
+ }
100237
+ function isPackageSpecifier(p5) {
100238
+ if (p5.startsWith(".") || import_node_path.default.isAbsolute(p5)) return false;
100239
+ if (p5.startsWith("@") || p5.includes(":")) return true;
100240
+ return !p5.includes("/");
100241
+ }
100242
+ function pointsAtDirectory(absPath) {
100243
+ try {
100244
+ return import_node_fs.default.statSync(absPath).isDirectory();
100245
+ } catch {
100246
+ for (const ext of [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs"]) {
100247
+ if (import_node_fs.default.existsSync(`${absPath}${ext}`)) return false;
100248
+ }
100249
+ return !/\.[a-z]+$/i.test(import_node_path.default.basename(absPath));
100250
+ }
100251
+ }
100252
+ function withTsExtension(p5) {
100253
+ if (/\.(ts|tsx|mts|cts)$/.test(p5)) return p5;
100254
+ if (/\.(js|mjs|cjs)$/.test(p5)) return p5.replace(/\.(js|mjs|cjs)$/, ".ts");
100255
+ return `${p5}.ts`;
100256
+ }
100257
+
100258
+ // src/naming.ts
100259
+ var NAME_MODES = ["insert", "update", "select"];
100260
+ var DEFAULT_MODE_PREFIX = {
100261
+ insert: "Insert",
100262
+ update: "Update",
100263
+ select: "Select"
100264
+ };
100265
+ var DEFAULT_TYPE_SUFFIX = {
100266
+ insert: "Input",
100267
+ update: "Input",
100268
+ select: "Output"
100269
+ };
100270
+ var DEFAULT_SCHEMA_SUFFIX = "Schema";
100271
+ var AFFIX_PROBE_TABLE = "users";
100272
+ function spread(value, fallback) {
100273
+ if (value === void 0) return { ...fallback };
100274
+ if (typeof value === "string") return { insert: value, update: value, select: value };
100275
+ return {
100276
+ insert: value.insert ?? fallback.insert,
100277
+ update: value.update ?? fallback.update,
100278
+ select: value.select ?? fallback.select
100279
+ };
100280
+ }
100281
+ function pascalCase(s) {
100282
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).filter(Boolean).map((p5) => p5.charAt(0).toUpperCase() + p5.slice(1)).join("");
100283
+ }
100284
+ function applyTableCase(tsName, tableCase) {
100285
+ return tableCase === "pascal" ? pascalCase(tsName) : tsName;
100286
+ }
100287
+ function resolveAffix(opts) {
100288
+ const affix = opts?.affix;
100289
+ const legacy = opts?.schemaSuffix ?? DEFAULT_SCHEMA_SUFFIX;
100290
+ const legacyMap = {
100291
+ insert: legacy,
100292
+ update: legacy,
100293
+ select: legacy
100294
+ };
100295
+ return {
100296
+ tableCase: affix?.tableCase ?? "preserve",
100297
+ schema: {
100298
+ prefix: spread(affix?.schema?.prefix, DEFAULT_MODE_PREFIX),
100299
+ suffix: spread(affix?.schema?.suffix, legacyMap)
100300
+ },
100301
+ type: {
100302
+ prefix: spread(affix?.type?.prefix, DEFAULT_MODE_PREFIX),
100303
+ suffix: spread(affix?.type?.suffix, DEFAULT_TYPE_SUFFIX)
100304
+ }
100305
+ };
100306
+ }
100307
+ function schemaName(mode, tsName, affix) {
100308
+ return affix.schema.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.schema.suffix[mode];
100309
+ }
100310
+ function typeName(mode, tsName, affix) {
100311
+ return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
100312
+ }
100313
+ var PREFIX_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
100314
+ var SUFFIX_RE = /^[A-Za-z0-9_$]+$/;
100315
+ function validateAffix(affix, schemaSuffix) {
100316
+ const issues = [];
100317
+ if (!affix) return issues;
100318
+ const checkOne = (value, path14, kind) => {
100319
+ if (value === "") return;
100320
+ const ok = kind === "prefix" ? PREFIX_RE.test(value) : SUFFIX_RE.test(value);
100321
+ if (ok) return;
100322
+ issues.push({
100323
+ path: path14,
100324
+ message: `${JSON.stringify(value)} cannot appear in a TypeScript identifier. Use only letters, digits, "_" and "$"` + (kind === "prefix" ? ", and do not start with a digit." : ".")
100325
+ });
100326
+ };
100327
+ const checkValue = (value, base, kind) => {
100328
+ if (value === void 0) return;
100329
+ if (typeof value === "string") {
100330
+ checkOne(value, base, kind);
100331
+ return;
100332
+ }
100333
+ for (const mode of NAME_MODES) {
100334
+ const v7 = value[mode];
100335
+ if (v7 !== void 0) checkOne(v7, [...base, mode], kind);
100336
+ }
100337
+ };
100338
+ checkValue(affix.schema?.prefix, ["schema", "prefix"], "prefix");
100339
+ checkValue(affix.schema?.suffix, ["schema", "suffix"], "suffix");
100340
+ checkValue(affix.type?.prefix, ["type", "prefix"], "prefix");
100341
+ checkValue(affix.type?.suffix, ["type", "suffix"], "suffix");
100342
+ if (issues.length) return issues;
100343
+ const resolved = resolveAffix({ affix, schemaSuffix });
100344
+ const collisions = (space, build) => {
100345
+ const seen = /* @__PURE__ */ new Map();
100346
+ for (const mode of NAME_MODES) {
100347
+ const name = build(mode);
100348
+ const first = seen.get(name);
100349
+ if (first) {
100350
+ issues.push({
100351
+ path: [space],
100352
+ message: `The ${space} names for "${first}" and "${mode}" collide: both resolve to "${name}". All three are emitted into the same file, so at least one prefix or suffix has to differ.`
100353
+ });
100354
+ } else {
100355
+ seen.set(name, mode);
100356
+ }
100357
+ }
100358
+ };
100359
+ collisions("schema", (mode) => schemaName(mode, AFFIX_PROBE_TABLE, resolved));
100360
+ collisions("type", (mode) => typeName(mode, AFFIX_PROBE_TABLE, resolved));
100361
+ return issues;
100362
+ }
100363
+
100364
+ // src/index.ts
100181
100365
  function isGeneratedColumn(c5, primaryKeyColumns) {
100182
100366
  return c5.isGenerated || primaryKeyColumns.includes(c5.name);
100183
100367
  }
@@ -100219,9 +100403,26 @@ async function formatCode(code, filePath, fmt) {
100219
100403
  }
100220
100404
  // Annotate the CommonJS export names for ESM import in node:
100221
100405
  0 && (module.exports = {
100406
+ AFFIX_PROBE_TABLE,
100407
+ DEFAULT_IMPORT_EXTENSION,
100408
+ DEFAULT_MODE_PREFIX,
100409
+ DEFAULT_SCHEMA_SUFFIX,
100410
+ DEFAULT_TYPE_SUFFIX,
100411
+ IMPORT_EXTENSIONS,
100412
+ NAME_MODES,
100413
+ applyTableCase,
100222
100414
  formatCode,
100415
+ importSpecifier,
100223
100416
  insertColumns,
100224
100417
  isGeneratedColumn,
100418
+ moduleFileName,
100419
+ moduleSpecifier,
100420
+ pascalCase,
100421
+ resolveAffix,
100422
+ resolveConfiguredImport,
100423
+ schemaName,
100225
100424
  selectColumns,
100226
- updateColumns
100425
+ typeName,
100426
+ updateColumns,
100427
+ validateAffix
100227
100428
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,178 @@
1
1
  import { Column, Analysis } from '@drzl/analyzer';
2
2
 
3
+ /**
4
+ * One place that decides what a generated module is called, on disk and in an import.
5
+ *
6
+ * The three validation generators used to name the file from `fileSuffix` but hardcode the
7
+ * default suffix in the barrel, so any custom `fileSuffix` produced `export * from
8
+ * './users.zod'` next to a file called `users.schema.ts` and the consumer's build failed on
9
+ * an unresolved import. Both halves are derived here from the same value now.
10
+ */
11
+ /**
12
+ * How a relative import of a generated file spells its extension.
13
+ *
14
+ * Generated files land in the consumer's own source tree, so the consumer's
15
+ * `moduleResolution` decides which forms resolve. Measured against tsc 5.9.2 and 7.0.2, for
16
+ * a specifier pointing at a sibling `.ts` file:
17
+ *
18
+ * | form | bundler | node10 | node16 / nodenext (CJS) | node16 / nodenext (ESM) |
19
+ * | --------------- | ------- | ------ | ----------------------- | ----------------------- |
20
+ * | `'js'` | yes | yes | yes | yes |
21
+ * | `'none'` | yes | yes | yes | **no** |
22
+ * | `'ts'` | flag | flag | flag | flag |
23
+ *
24
+ * `'js'` is the default because it is the only form that needs no compiler flag and still
25
+ * resolves in every cell. `'none'` is what drzl emitted before 2.0 and is what a pipeline
26
+ * that cannot map `.js` back to `.ts` wants (webpack without `resolve.extensionAlias`,
27
+ * ts-jest without a `moduleNameMapper`). `'ts'` needs `allowImportingTsExtensions`, and is
28
+ * the only form Node's own type stripping accepts, so it suits a project that runs the
29
+ * generated `.ts` unbuilt.
30
+ */
31
+ type ImportExtension = (typeof IMPORT_EXTENSIONS)[number];
32
+ /**
33
+ * Every value `ImportExtension` accepts, in the order documentation lists them. The type is
34
+ * derived from this tuple rather than declared alongside it, so a config schema built from
35
+ * it accepts exactly what the type allows and the two cannot drift.
36
+ */
37
+ declare const IMPORT_EXTENSIONS: readonly ["js", "none", "ts"];
38
+ /** What `importExtension` means when nothing sets it. */
39
+ declare const DEFAULT_IMPORT_EXTENSION: ImportExtension;
40
+ /** Name of the file a table is written to, e.g. `users.zod.ts`. */
41
+ declare function moduleFileName(tsName: string, fileSuffix: string): string;
42
+ /**
43
+ * Rewrite the extension of a relative path naming a generated file into the form an import
44
+ * specifier has to spell, e.g. `./users.zod.ts` -> `./users.zod.js`.
45
+ *
46
+ * A path that ends in no TypeScript extension is left whole: such a file cannot be imported
47
+ * at all, and naming a neighbour that does not exist would only hide that.
48
+ */
49
+ declare function importSpecifier(relativePath: string, importExtension?: ImportExtension): string;
50
+ /**
51
+ * Relative specifier a sibling module needs to import a table's file, e.g.
52
+ * `./users.zod.js`.
53
+ */
54
+ declare function moduleSpecifier(tsName: string, fileSuffix: string, importExtension?: ImportExtension): string;
55
+ /**
56
+ * Turn a configured module path into a specifier the generated file can actually import.
57
+ *
58
+ * Options like `validation.importPath`, `dbImportPath` and `schemaImportPath` get written as
59
+ * project-relative paths, `src/validators/zod`, because that is how the rest of the config
60
+ * names directories. Emitted verbatim that is a *bare* specifier: Node and tsc look for a
61
+ * package of that name in node_modules and never consider the local file. The config in the
62
+ * getting-started guide produced three such imports, none of which resolved.
63
+ *
64
+ * Two questions have to be answered, and neither can be guessed from the string alone.
65
+ *
66
+ * **Package or path?** `zod` and `@acme/schemas` are package names and pass through untouched.
67
+ * A path containing a separator, or starting with `.`, is a path.
68
+ *
69
+ * **File or directory?** `src/db/connection` is usually a file and `src/validators/zod` a
70
+ * directory holding a barrel, and they look identical. So the filesystem is asked. Where the
71
+ * target does not exist yet, which happens when this generator runs before the one that writes
72
+ * it, an extensionless path is taken to be a directory, since these options name directories by
73
+ * convention and the one path that can be missing is a generated barrel.
74
+ *
75
+ * A path already relative keeps its own spelling and only has its extension corrected, so
76
+ * anyone who followed the older guidance and wrote `../validators/zod/index.js` is unaffected.
77
+ *
78
+ * @param configured what the user put in the config
79
+ * @param outDirAbs absolute directory the importing file is written to
80
+ * @param cwd project root a non-relative path is resolved against
81
+ */
82
+ declare function resolveConfiguredImport(configured: string, outDirAbs: string, cwd: string, importExtension?: ImportExtension): string;
83
+
84
+ /**
85
+ * One place that decides what a generated identifier is called.
86
+ *
87
+ * Every generator used to build these names by hand with template literals, which is why
88
+ * the oRPC router and the zod/valibot/arktype generators could silently disagree about the
89
+ * same name. They all call through here now, so a single resolved value describes both
90
+ * sides of the import.
91
+ *
92
+ * Defaults reproduce the pre-affix output byte for byte:
93
+ * schema: Insert|Update|Select + <tsName> + (schemaSuffix ?? 'Schema')
94
+ * type: Insert|Update|Select + <tsName> + Input|Input|Output
95
+ */
96
+ type NameMode = 'insert' | 'update' | 'select';
97
+ /** How the Drizzle export name is cased before it goes into an identifier. */
98
+ type TableCase = 'preserve' | 'pascal';
99
+ /** One affix for all three modes, or a per-mode override map. */
100
+ type AffixValue = string | Partial<Record<NameMode, string>>;
101
+ interface AffixOptions {
102
+ /**
103
+ * `preserve` (default) interpolates the Drizzle export name verbatim, which is what every
104
+ * released version does: `export const users` yields `InsertusersSchema`. `pascal` upper-camels
105
+ * it first, yielding `InsertUsersSchema`. Identifiers only; file names are never re-cased.
106
+ */
107
+ tableCase?: TableCase;
108
+ /** Affixes for the exported schema constants. */
109
+ schema?: {
110
+ prefix?: AffixValue;
111
+ suffix?: AffixValue;
112
+ };
113
+ /** Affixes for the exported type aliases. Independent of `schema`. */
114
+ type?: {
115
+ prefix?: AffixValue;
116
+ suffix?: AffixValue;
117
+ };
118
+ }
119
+ interface ResolvedAffix {
120
+ tableCase: TableCase;
121
+ schema: {
122
+ prefix: Record<NameMode, string>;
123
+ suffix: Record<NameMode, string>;
124
+ };
125
+ type: {
126
+ prefix: Record<NameMode, string>;
127
+ suffix: Record<NameMode, string>;
128
+ };
129
+ }
130
+ interface AffixIssue {
131
+ /** Path relative to the affix object, e.g. `['schema', 'suffix']`. */
132
+ path: (string | number)[];
133
+ message: string;
134
+ }
135
+ declare const NAME_MODES: readonly NameMode[];
136
+ declare const DEFAULT_MODE_PREFIX: Readonly<Record<NameMode, string>>;
137
+ declare const DEFAULT_TYPE_SUFFIX: Readonly<Record<NameMode, string>>;
138
+ declare const DEFAULT_SCHEMA_SUFFIX = "Schema";
139
+ /** Table name used when a config is checked for invalid or colliding names. */
140
+ declare const AFFIX_PROBE_TABLE = "users";
141
+ /**
142
+ * Real PascalCase, unlike the `cap()` helpers scattered around the repo which only upcase
143
+ * character zero. Splits on `_`, `-`, whitespace and camel boundaries, and leaves the rest of
144
+ * each part alone so acronyms survive (`userID` -> `UserID`, not `Userid`).
145
+ */
146
+ declare function pascalCase(s: string): string;
147
+ declare function applyTableCase(tsName: string, tableCase: TableCase): string;
148
+ /**
149
+ * Fold an `affix` block and the legacy flat `schemaSuffix` into one fully-populated value.
150
+ * `affix.schema.suffix` wins over `schemaSuffix`; `schemaSuffix` wins over the built-in
151
+ * `'Schema'`. Calling with no arguments returns exactly today's naming.
152
+ */
153
+ declare function resolveAffix(opts?: {
154
+ affix?: AffixOptions;
155
+ schemaSuffix?: string;
156
+ }): ResolvedAffix;
157
+ /** Name of the exported schema constant, e.g. `InsertusersSchema`. */
158
+ declare function schemaName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
159
+ /** Name of the exported type alias, e.g. `InsertusersInput`. */
160
+ declare function typeName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
161
+ /**
162
+ * Reject affixes that cannot produce a compilable file, before anything is written:
163
+ * - characters that are not legal in a TypeScript identifier
164
+ * - two names in the same declaration space resolving to the same string
165
+ *
166
+ * A schema name equal to a type name is allowed on purpose: `export const X` and
167
+ * `export type X` occupy different declaration spaces, and the generators already emit
168
+ * `type ... = z.input<typeof ...>` pairs.
169
+ *
170
+ * Only what the caller actually wrote in `affix` is checked. The legacy flat `schemaSuffix`
171
+ * is not character-checked, because it never was, and rejecting it now would break configs
172
+ * that parse today.
173
+ */
174
+ declare function validateAffix(affix?: AffixOptions, schemaSuffix?: string): AffixIssue[];
175
+
3
176
  interface Table {
4
177
  name: string;
5
178
  tsName: string;
@@ -17,8 +190,26 @@ interface FormatOptions {
17
190
  interface ValidationGenerateOptions {
18
191
  outDir: string;
19
192
  format?: FormatOptions;
193
+ /**
194
+ * What every generated file is called after the Drizzle export name, e.g. `.zod.ts`
195
+ * yields `users.zod.ts`. The barrel derives its import specifiers from this same value,
196
+ * so a custom suffix keeps resolving.
197
+ */
20
198
  fileSuffix?: string;
199
+ /**
200
+ * How the barrel spells the extension of the files it re-exports. Defaults to `'js'`,
201
+ * so `users.zod.ts` is imported as `./users.zod.js`, the only form that resolves under
202
+ * every `moduleResolution` without a compiler flag. Use `'none'` for the extensionless
203
+ * specifiers drzl emitted before 2.0.
204
+ */
205
+ importExtension?: ImportExtension;
21
206
  schemaSuffix?: string;
207
+ /**
208
+ * Prefixes, suffixes and table casing for the generated identifiers. Omit it and the
209
+ * output is identical to every previous version; `schemaSuffix` stays the fallback for
210
+ * `affix.schema.suffix`.
211
+ */
212
+ affix?: AffixOptions;
22
213
  coerceDates?: 'input' | 'all' | 'none';
23
214
  emit?: {
24
215
  select?: boolean;
@@ -38,4 +229,4 @@ declare function updateColumns(table: Table): Column[];
38
229
  declare function selectColumns(table: Table): Column[];
39
230
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
40
231
 
41
- export { type FormatOptions, type Table, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, formatCode, insertColumns, isGeneratedColumn, selectColumns, updateColumns };
232
+ export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, NAME_MODES, type NameMode, type ResolvedAffix, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, formatCode, importSpecifier, insertColumns, isGeneratedColumn, moduleFileName, moduleSpecifier, pascalCase, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,178 @@
1
1
  import { Column, Analysis } from '@drzl/analyzer';
2
2
 
3
+ /**
4
+ * One place that decides what a generated module is called, on disk and in an import.
5
+ *
6
+ * The three validation generators used to name the file from `fileSuffix` but hardcode the
7
+ * default suffix in the barrel, so any custom `fileSuffix` produced `export * from
8
+ * './users.zod'` next to a file called `users.schema.ts` and the consumer's build failed on
9
+ * an unresolved import. Both halves are derived here from the same value now.
10
+ */
11
+ /**
12
+ * How a relative import of a generated file spells its extension.
13
+ *
14
+ * Generated files land in the consumer's own source tree, so the consumer's
15
+ * `moduleResolution` decides which forms resolve. Measured against tsc 5.9.2 and 7.0.2, for
16
+ * a specifier pointing at a sibling `.ts` file:
17
+ *
18
+ * | form | bundler | node10 | node16 / nodenext (CJS) | node16 / nodenext (ESM) |
19
+ * | --------------- | ------- | ------ | ----------------------- | ----------------------- |
20
+ * | `'js'` | yes | yes | yes | yes |
21
+ * | `'none'` | yes | yes | yes | **no** |
22
+ * | `'ts'` | flag | flag | flag | flag |
23
+ *
24
+ * `'js'` is the default because it is the only form that needs no compiler flag and still
25
+ * resolves in every cell. `'none'` is what drzl emitted before 2.0 and is what a pipeline
26
+ * that cannot map `.js` back to `.ts` wants (webpack without `resolve.extensionAlias`,
27
+ * ts-jest without a `moduleNameMapper`). `'ts'` needs `allowImportingTsExtensions`, and is
28
+ * the only form Node's own type stripping accepts, so it suits a project that runs the
29
+ * generated `.ts` unbuilt.
30
+ */
31
+ type ImportExtension = (typeof IMPORT_EXTENSIONS)[number];
32
+ /**
33
+ * Every value `ImportExtension` accepts, in the order documentation lists them. The type is
34
+ * derived from this tuple rather than declared alongside it, so a config schema built from
35
+ * it accepts exactly what the type allows and the two cannot drift.
36
+ */
37
+ declare const IMPORT_EXTENSIONS: readonly ["js", "none", "ts"];
38
+ /** What `importExtension` means when nothing sets it. */
39
+ declare const DEFAULT_IMPORT_EXTENSION: ImportExtension;
40
+ /** Name of the file a table is written to, e.g. `users.zod.ts`. */
41
+ declare function moduleFileName(tsName: string, fileSuffix: string): string;
42
+ /**
43
+ * Rewrite the extension of a relative path naming a generated file into the form an import
44
+ * specifier has to spell, e.g. `./users.zod.ts` -> `./users.zod.js`.
45
+ *
46
+ * A path that ends in no TypeScript extension is left whole: such a file cannot be imported
47
+ * at all, and naming a neighbour that does not exist would only hide that.
48
+ */
49
+ declare function importSpecifier(relativePath: string, importExtension?: ImportExtension): string;
50
+ /**
51
+ * Relative specifier a sibling module needs to import a table's file, e.g.
52
+ * `./users.zod.js`.
53
+ */
54
+ declare function moduleSpecifier(tsName: string, fileSuffix: string, importExtension?: ImportExtension): string;
55
+ /**
56
+ * Turn a configured module path into a specifier the generated file can actually import.
57
+ *
58
+ * Options like `validation.importPath`, `dbImportPath` and `schemaImportPath` get written as
59
+ * project-relative paths, `src/validators/zod`, because that is how the rest of the config
60
+ * names directories. Emitted verbatim that is a *bare* specifier: Node and tsc look for a
61
+ * package of that name in node_modules and never consider the local file. The config in the
62
+ * getting-started guide produced three such imports, none of which resolved.
63
+ *
64
+ * Two questions have to be answered, and neither can be guessed from the string alone.
65
+ *
66
+ * **Package or path?** `zod` and `@acme/schemas` are package names and pass through untouched.
67
+ * A path containing a separator, or starting with `.`, is a path.
68
+ *
69
+ * **File or directory?** `src/db/connection` is usually a file and `src/validators/zod` a
70
+ * directory holding a barrel, and they look identical. So the filesystem is asked. Where the
71
+ * target does not exist yet, which happens when this generator runs before the one that writes
72
+ * it, an extensionless path is taken to be a directory, since these options name directories by
73
+ * convention and the one path that can be missing is a generated barrel.
74
+ *
75
+ * A path already relative keeps its own spelling and only has its extension corrected, so
76
+ * anyone who followed the older guidance and wrote `../validators/zod/index.js` is unaffected.
77
+ *
78
+ * @param configured what the user put in the config
79
+ * @param outDirAbs absolute directory the importing file is written to
80
+ * @param cwd project root a non-relative path is resolved against
81
+ */
82
+ declare function resolveConfiguredImport(configured: string, outDirAbs: string, cwd: string, importExtension?: ImportExtension): string;
83
+
84
+ /**
85
+ * One place that decides what a generated identifier is called.
86
+ *
87
+ * Every generator used to build these names by hand with template literals, which is why
88
+ * the oRPC router and the zod/valibot/arktype generators could silently disagree about the
89
+ * same name. They all call through here now, so a single resolved value describes both
90
+ * sides of the import.
91
+ *
92
+ * Defaults reproduce the pre-affix output byte for byte:
93
+ * schema: Insert|Update|Select + <tsName> + (schemaSuffix ?? 'Schema')
94
+ * type: Insert|Update|Select + <tsName> + Input|Input|Output
95
+ */
96
+ type NameMode = 'insert' | 'update' | 'select';
97
+ /** How the Drizzle export name is cased before it goes into an identifier. */
98
+ type TableCase = 'preserve' | 'pascal';
99
+ /** One affix for all three modes, or a per-mode override map. */
100
+ type AffixValue = string | Partial<Record<NameMode, string>>;
101
+ interface AffixOptions {
102
+ /**
103
+ * `preserve` (default) interpolates the Drizzle export name verbatim, which is what every
104
+ * released version does: `export const users` yields `InsertusersSchema`. `pascal` upper-camels
105
+ * it first, yielding `InsertUsersSchema`. Identifiers only; file names are never re-cased.
106
+ */
107
+ tableCase?: TableCase;
108
+ /** Affixes for the exported schema constants. */
109
+ schema?: {
110
+ prefix?: AffixValue;
111
+ suffix?: AffixValue;
112
+ };
113
+ /** Affixes for the exported type aliases. Independent of `schema`. */
114
+ type?: {
115
+ prefix?: AffixValue;
116
+ suffix?: AffixValue;
117
+ };
118
+ }
119
+ interface ResolvedAffix {
120
+ tableCase: TableCase;
121
+ schema: {
122
+ prefix: Record<NameMode, string>;
123
+ suffix: Record<NameMode, string>;
124
+ };
125
+ type: {
126
+ prefix: Record<NameMode, string>;
127
+ suffix: Record<NameMode, string>;
128
+ };
129
+ }
130
+ interface AffixIssue {
131
+ /** Path relative to the affix object, e.g. `['schema', 'suffix']`. */
132
+ path: (string | number)[];
133
+ message: string;
134
+ }
135
+ declare const NAME_MODES: readonly NameMode[];
136
+ declare const DEFAULT_MODE_PREFIX: Readonly<Record<NameMode, string>>;
137
+ declare const DEFAULT_TYPE_SUFFIX: Readonly<Record<NameMode, string>>;
138
+ declare const DEFAULT_SCHEMA_SUFFIX = "Schema";
139
+ /** Table name used when a config is checked for invalid or colliding names. */
140
+ declare const AFFIX_PROBE_TABLE = "users";
141
+ /**
142
+ * Real PascalCase, unlike the `cap()` helpers scattered around the repo which only upcase
143
+ * character zero. Splits on `_`, `-`, whitespace and camel boundaries, and leaves the rest of
144
+ * each part alone so acronyms survive (`userID` -> `UserID`, not `Userid`).
145
+ */
146
+ declare function pascalCase(s: string): string;
147
+ declare function applyTableCase(tsName: string, tableCase: TableCase): string;
148
+ /**
149
+ * Fold an `affix` block and the legacy flat `schemaSuffix` into one fully-populated value.
150
+ * `affix.schema.suffix` wins over `schemaSuffix`; `schemaSuffix` wins over the built-in
151
+ * `'Schema'`. Calling with no arguments returns exactly today's naming.
152
+ */
153
+ declare function resolveAffix(opts?: {
154
+ affix?: AffixOptions;
155
+ schemaSuffix?: string;
156
+ }): ResolvedAffix;
157
+ /** Name of the exported schema constant, e.g. `InsertusersSchema`. */
158
+ declare function schemaName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
159
+ /** Name of the exported type alias, e.g. `InsertusersInput`. */
160
+ declare function typeName(mode: NameMode, tsName: string, affix: ResolvedAffix): string;
161
+ /**
162
+ * Reject affixes that cannot produce a compilable file, before anything is written:
163
+ * - characters that are not legal in a TypeScript identifier
164
+ * - two names in the same declaration space resolving to the same string
165
+ *
166
+ * A schema name equal to a type name is allowed on purpose: `export const X` and
167
+ * `export type X` occupy different declaration spaces, and the generators already emit
168
+ * `type ... = z.input<typeof ...>` pairs.
169
+ *
170
+ * Only what the caller actually wrote in `affix` is checked. The legacy flat `schemaSuffix`
171
+ * is not character-checked, because it never was, and rejecting it now would break configs
172
+ * that parse today.
173
+ */
174
+ declare function validateAffix(affix?: AffixOptions, schemaSuffix?: string): AffixIssue[];
175
+
3
176
  interface Table {
4
177
  name: string;
5
178
  tsName: string;
@@ -17,8 +190,26 @@ interface FormatOptions {
17
190
  interface ValidationGenerateOptions {
18
191
  outDir: string;
19
192
  format?: FormatOptions;
193
+ /**
194
+ * What every generated file is called after the Drizzle export name, e.g. `.zod.ts`
195
+ * yields `users.zod.ts`. The barrel derives its import specifiers from this same value,
196
+ * so a custom suffix keeps resolving.
197
+ */
20
198
  fileSuffix?: string;
199
+ /**
200
+ * How the barrel spells the extension of the files it re-exports. Defaults to `'js'`,
201
+ * so `users.zod.ts` is imported as `./users.zod.js`, the only form that resolves under
202
+ * every `moduleResolution` without a compiler flag. Use `'none'` for the extensionless
203
+ * specifiers drzl emitted before 2.0.
204
+ */
205
+ importExtension?: ImportExtension;
21
206
  schemaSuffix?: string;
207
+ /**
208
+ * Prefixes, suffixes and table casing for the generated identifiers. Omit it and the
209
+ * output is identical to every previous version; `schemaSuffix` stays the fallback for
210
+ * `affix.schema.suffix`.
211
+ */
212
+ affix?: AffixOptions;
22
213
  coerceDates?: 'input' | 'all' | 'none';
23
214
  emit?: {
24
215
  select?: boolean;
@@ -38,4 +229,4 @@ declare function updateColumns(table: Table): Column[];
38
229
  declare function selectColumns(table: Table): Column[];
39
230
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
40
231
 
41
- export { type FormatOptions, type Table, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, formatCode, insertColumns, isGeneratedColumn, selectColumns, updateColumns };
232
+ export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, NAME_MODES, type NameMode, type ResolvedAffix, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, formatCode, importSpecifier, insertColumns, isGeneratedColumn, moduleFileName, moduleSpecifier, pascalCase, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
package/dist/index.js CHANGED
@@ -1,5 +1,170 @@
1
1
  import "./chunk-MKBO26DX.js";
2
2
 
3
+ // src/files.ts
4
+ import nodeFs from "fs";
5
+ import nodePath from "path";
6
+ var IMPORT_EXTENSIONS = ["js", "none", "ts"];
7
+ var DEFAULT_IMPORT_EXTENSION = "js";
8
+ var TS_EXTENSIONS = [
9
+ { ext: ".mts", js: ".mjs", none: ".mjs" },
10
+ { ext: ".cts", js: ".cjs", none: ".cjs" },
11
+ { ext: ".tsx", js: ".js", none: "" },
12
+ { ext: ".ts", js: ".js", none: "" }
13
+ ];
14
+ function moduleFileName(tsName, fileSuffix) {
15
+ return `${tsName}${fileSuffix}`;
16
+ }
17
+ function importSpecifier(relativePath, importExtension = DEFAULT_IMPORT_EXTENSION) {
18
+ for (const { ext, js, none } of TS_EXTENSIONS) {
19
+ if (!relativePath.endsWith(ext)) continue;
20
+ const stem = relativePath.slice(0, -ext.length);
21
+ if (importExtension === "ts") return relativePath;
22
+ return `${stem}${importExtension === "none" ? none : js}`;
23
+ }
24
+ return relativePath;
25
+ }
26
+ function moduleSpecifier(tsName, fileSuffix, importExtension = DEFAULT_IMPORT_EXTENSION) {
27
+ return importSpecifier(`./${moduleFileName(tsName, fileSuffix)}`, importExtension);
28
+ }
29
+ function resolveConfiguredImport(configured, outDirAbs, cwd, importExtension = DEFAULT_IMPORT_EXTENSION) {
30
+ if (isPackageSpecifier(configured)) return configured;
31
+ const targetAbs = nodePath.isAbsolute(configured) ? configured : nodePath.resolve(configured.startsWith(".") ? outDirAbs : cwd, configured);
32
+ const withIndex = pointsAtDirectory(targetAbs) ? `${configured}/index` : configured;
33
+ if (configured.startsWith(".")) {
34
+ return importSpecifier(withTsExtension(withIndex), importExtension);
35
+ }
36
+ const resolved = pointsAtDirectory(targetAbs) ? nodePath.join(targetAbs, "index") : targetAbs;
37
+ const rel = nodePath.relative(outDirAbs, resolved).split(nodePath.sep).join("/");
38
+ const prefixed = !rel ? "." : rel.startsWith(".") ? rel : `./${rel}`;
39
+ return importSpecifier(withTsExtension(prefixed), importExtension);
40
+ }
41
+ function isPackageSpecifier(p) {
42
+ if (p.startsWith(".") || nodePath.isAbsolute(p)) return false;
43
+ if (p.startsWith("@") || p.includes(":")) return true;
44
+ return !p.includes("/");
45
+ }
46
+ function pointsAtDirectory(absPath) {
47
+ try {
48
+ return nodeFs.statSync(absPath).isDirectory();
49
+ } catch {
50
+ for (const ext of [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs"]) {
51
+ if (nodeFs.existsSync(`${absPath}${ext}`)) return false;
52
+ }
53
+ return !/\.[a-z]+$/i.test(nodePath.basename(absPath));
54
+ }
55
+ }
56
+ function withTsExtension(p) {
57
+ if (/\.(ts|tsx|mts|cts)$/.test(p)) return p;
58
+ if (/\.(js|mjs|cjs)$/.test(p)) return p.replace(/\.(js|mjs|cjs)$/, ".ts");
59
+ return `${p}.ts`;
60
+ }
61
+
62
+ // src/naming.ts
63
+ var NAME_MODES = ["insert", "update", "select"];
64
+ var DEFAULT_MODE_PREFIX = {
65
+ insert: "Insert",
66
+ update: "Update",
67
+ select: "Select"
68
+ };
69
+ var DEFAULT_TYPE_SUFFIX = {
70
+ insert: "Input",
71
+ update: "Input",
72
+ select: "Output"
73
+ };
74
+ var DEFAULT_SCHEMA_SUFFIX = "Schema";
75
+ var AFFIX_PROBE_TABLE = "users";
76
+ function spread(value, fallback) {
77
+ if (value === void 0) return { ...fallback };
78
+ if (typeof value === "string") return { insert: value, update: value, select: value };
79
+ return {
80
+ insert: value.insert ?? fallback.insert,
81
+ update: value.update ?? fallback.update,
82
+ select: value.select ?? fallback.select
83
+ };
84
+ }
85
+ function pascalCase(s) {
86
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
87
+ }
88
+ function applyTableCase(tsName, tableCase) {
89
+ return tableCase === "pascal" ? pascalCase(tsName) : tsName;
90
+ }
91
+ function resolveAffix(opts) {
92
+ const affix = opts?.affix;
93
+ const legacy = opts?.schemaSuffix ?? DEFAULT_SCHEMA_SUFFIX;
94
+ const legacyMap = {
95
+ insert: legacy,
96
+ update: legacy,
97
+ select: legacy
98
+ };
99
+ return {
100
+ tableCase: affix?.tableCase ?? "preserve",
101
+ schema: {
102
+ prefix: spread(affix?.schema?.prefix, DEFAULT_MODE_PREFIX),
103
+ suffix: spread(affix?.schema?.suffix, legacyMap)
104
+ },
105
+ type: {
106
+ prefix: spread(affix?.type?.prefix, DEFAULT_MODE_PREFIX),
107
+ suffix: spread(affix?.type?.suffix, DEFAULT_TYPE_SUFFIX)
108
+ }
109
+ };
110
+ }
111
+ function schemaName(mode, tsName, affix) {
112
+ return affix.schema.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.schema.suffix[mode];
113
+ }
114
+ function typeName(mode, tsName, affix) {
115
+ return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
116
+ }
117
+ var PREFIX_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
118
+ var SUFFIX_RE = /^[A-Za-z0-9_$]+$/;
119
+ function validateAffix(affix, schemaSuffix) {
120
+ const issues = [];
121
+ if (!affix) return issues;
122
+ const checkOne = (value, path, kind) => {
123
+ if (value === "") return;
124
+ const ok = kind === "prefix" ? PREFIX_RE.test(value) : SUFFIX_RE.test(value);
125
+ if (ok) return;
126
+ issues.push({
127
+ path,
128
+ message: `${JSON.stringify(value)} cannot appear in a TypeScript identifier. Use only letters, digits, "_" and "$"` + (kind === "prefix" ? ", and do not start with a digit." : ".")
129
+ });
130
+ };
131
+ const checkValue = (value, base, kind) => {
132
+ if (value === void 0) return;
133
+ if (typeof value === "string") {
134
+ checkOne(value, base, kind);
135
+ return;
136
+ }
137
+ for (const mode of NAME_MODES) {
138
+ const v = value[mode];
139
+ if (v !== void 0) checkOne(v, [...base, mode], kind);
140
+ }
141
+ };
142
+ checkValue(affix.schema?.prefix, ["schema", "prefix"], "prefix");
143
+ checkValue(affix.schema?.suffix, ["schema", "suffix"], "suffix");
144
+ checkValue(affix.type?.prefix, ["type", "prefix"], "prefix");
145
+ checkValue(affix.type?.suffix, ["type", "suffix"], "suffix");
146
+ if (issues.length) return issues;
147
+ const resolved = resolveAffix({ affix, schemaSuffix });
148
+ const collisions = (space, build) => {
149
+ const seen = /* @__PURE__ */ new Map();
150
+ for (const mode of NAME_MODES) {
151
+ const name = build(mode);
152
+ const first = seen.get(name);
153
+ if (first) {
154
+ issues.push({
155
+ path: [space],
156
+ message: `The ${space} names for "${first}" and "${mode}" collide: both resolve to "${name}". All three are emitted into the same file, so at least one prefix or suffix has to differ.`
157
+ });
158
+ } else {
159
+ seen.set(name, mode);
160
+ }
161
+ }
162
+ };
163
+ collisions("schema", (mode) => schemaName(mode, AFFIX_PROBE_TABLE, resolved));
164
+ collisions("type", (mode) => typeName(mode, AFFIX_PROBE_TABLE, resolved));
165
+ return issues;
166
+ }
167
+
3
168
  // src/index.ts
4
169
  function isGeneratedColumn(c, primaryKeyColumns) {
5
170
  return c.isGenerated || primaryKeyColumns.includes(c.name);
@@ -41,9 +206,26 @@ async function formatCode(code, filePath, fmt) {
41
206
  return code;
42
207
  }
43
208
  export {
209
+ AFFIX_PROBE_TABLE,
210
+ DEFAULT_IMPORT_EXTENSION,
211
+ DEFAULT_MODE_PREFIX,
212
+ DEFAULT_SCHEMA_SUFFIX,
213
+ DEFAULT_TYPE_SUFFIX,
214
+ IMPORT_EXTENSIONS,
215
+ NAME_MODES,
216
+ applyTableCase,
44
217
  formatCode,
218
+ importSpecifier,
45
219
  insertColumns,
46
220
  isGeneratedColumn,
221
+ moduleFileName,
222
+ moduleSpecifier,
223
+ pascalCase,
224
+ resolveAffix,
225
+ resolveConfiguredImport,
226
+ schemaName,
47
227
  selectColumns,
48
- updateColumns
228
+ typeName,
229
+ updateColumns,
230
+ validateAffix
49
231
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/validation-core",
3
- "version": "1.1.0",
3
+ "version": "2.1.0",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "sideEffects": false,
13
13
  "dependencies": {
14
- "@drzl/analyzer": "^1.2.0"
14
+ "@drzl/analyzer": "^1.4.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "tsup": "^8.5.0",