@quonfig/cli 0.0.16 → 0.0.17
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 +33 -33
- package/dist/commands/migrate.js +47 -1
- package/dist/migrate/local-write.d.ts +7 -1
- package/dist/migrate/local-write.js +54 -6
- package/dist/migrate/migration-report.d.ts +18 -0
- package/dist/migrate/migration-report.js +64 -1
- package/dist/migrate/push-to-cloud.d.ts +13 -2
- package/dist/migrate/push-to-cloud.js +76 -5
- package/dist/migrate/source.d.ts +50 -1
- package/dist/migrate/sources/launch/translate.d.ts +27 -1
- package/dist/migrate/sources/launch/translate.js +123 -6
- package/dist/migrate/sources/launch/types.d.ts +3 -0
- package/dist/migrate/sources/launch/zod-to-json-schema.d.ts +9 -0
- package/dist/migrate/sources/launch/zod-to-json-schema.js +521 -0
- package/dist/migrate/sources/launch.js +52 -7
- package/dist/util/get-client.js +5 -4
- package/dist/util/get-valid-token.d.ts +3 -1
- package/dist/util/get-valid-token.js +28 -3
- package/oclif.manifest.json +1 -1
- package/package.json +3 -3
|
@@ -2,24 +2,94 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { removeQfFromGitignore, writeImportState } from './import-state.js';
|
|
4
4
|
import { writeMigrationReport } from './migration-report.js';
|
|
5
|
+
import { detectDuplicateKeys } from './sources/launch/translate.js';
|
|
5
6
|
import { cloneAndStackPush } from '../util/clone-and-stack-push.js';
|
|
6
7
|
const writeQuonfigFiles = (dir, changes, source) => {
|
|
8
|
+
const livePaths = new Map();
|
|
7
9
|
for (const change of changes) {
|
|
8
10
|
const files = source.translate(change);
|
|
9
11
|
for (const file of files) {
|
|
10
12
|
const full = path.join(dir, file.path);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
if (file.deleted) {
|
|
14
|
+
try {
|
|
15
|
+
fs.unlinkSync(full);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code !== 'ENOENT')
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
livePaths.delete(file.path);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
if (file.contents === undefined) {
|
|
25
|
+
throw new Error(`translate() emitted a write op for ${file.path} without contents`);
|
|
26
|
+
}
|
|
27
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
28
|
+
fs.writeFileSync(full, file.contents);
|
|
29
|
+
livePaths.set(file.path, true);
|
|
30
|
+
}
|
|
13
31
|
}
|
|
14
32
|
}
|
|
33
|
+
const resolutions = detectDuplicateKeys([...livePaths.keys()].map((p) => ({ path: p })));
|
|
34
|
+
for (const resolution of resolutions) {
|
|
35
|
+
for (const toDelete of resolution.deleted) {
|
|
36
|
+
try {
|
|
37
|
+
fs.unlinkSync(path.join(dir, toDelete));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code !== 'ENOENT')
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
livePaths.delete(toDelete);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return resolutions;
|
|
47
|
+
};
|
|
48
|
+
const QUONFIG_JSON_FILENAME = 'quonfig.json';
|
|
49
|
+
const mergeEnvironmentsIntoQuonfigJson = (dir, sourceEnvs) => {
|
|
50
|
+
const quonfigPath = path.join(dir, QUONFIG_JSON_FILENAME);
|
|
51
|
+
let existing = {};
|
|
52
|
+
if (fs.existsSync(quonfigPath)) {
|
|
53
|
+
try {
|
|
54
|
+
existing = JSON.parse(fs.readFileSync(quonfigPath, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
existing = {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const existingEnvs = Array.isArray(existing.environments) ? existing.environments : [];
|
|
61
|
+
const merged = [...new Set([...existingEnvs, ...sourceEnvs])].sort();
|
|
62
|
+
if (existingEnvs.length === merged.length &&
|
|
63
|
+
existingEnvs.every((e, i) => e === merged[i])) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
fs.writeFileSync(quonfigPath, JSON.stringify({ ...existing, environments: merged }, null, 2) + '\n', 'utf8');
|
|
15
67
|
};
|
|
16
68
|
export const pushMigrationToCloud = async (opts) => {
|
|
69
|
+
let droppedOverrides = null;
|
|
70
|
+
let duplicateResolutions = null;
|
|
71
|
+
let skippedConfigs = null;
|
|
17
72
|
const cloneOpts = {
|
|
18
73
|
applyDelta(dir) {
|
|
19
|
-
|
|
74
|
+
if (opts.environments && opts.environments.length > 0) {
|
|
75
|
+
mergeEnvironmentsIntoQuonfigJson(dir, opts.environments);
|
|
76
|
+
}
|
|
77
|
+
const resolutionEntries = writeQuonfigFiles(dir, opts.changes, opts.source);
|
|
20
78
|
removeQfFromGitignore(dir);
|
|
21
79
|
writeImportState(dir, opts.importState);
|
|
22
|
-
|
|
80
|
+
droppedOverrides = opts.source.getDroppedOverrides?.() ?? null;
|
|
81
|
+
skippedConfigs = opts.source.getSkippedConfigs?.() ?? null;
|
|
82
|
+
duplicateResolutions =
|
|
83
|
+
resolutionEntries.length > 0
|
|
84
|
+
? { entries: resolutionEntries, total: resolutionEntries.length }
|
|
85
|
+
: null;
|
|
86
|
+
const reportData = {
|
|
87
|
+
...opts.reportData,
|
|
88
|
+
...(droppedOverrides ? { droppedOverrides } : {}),
|
|
89
|
+
...(duplicateResolutions ? { duplicateResolutions } : {}),
|
|
90
|
+
...(skippedConfigs ? { skippedConfigs } : {}),
|
|
91
|
+
};
|
|
92
|
+
writeMigrationReport(dir, reportData);
|
|
23
93
|
},
|
|
24
94
|
commitMessage: opts.commitMessage,
|
|
25
95
|
localDir: opts.localDir,
|
|
@@ -27,5 +97,6 @@ export const pushMigrationToCloud = async (opts) => {
|
|
|
27
97
|
};
|
|
28
98
|
if (opts.branch !== undefined)
|
|
29
99
|
cloneOpts.branch = opts.branch;
|
|
30
|
-
|
|
100
|
+
const result = await cloneAndStackPush(cloneOpts);
|
|
101
|
+
return { ...result, droppedOverrides, duplicateResolutions, skippedConfigs };
|
|
31
102
|
};
|
package/dist/migrate/source.d.ts
CHANGED
|
@@ -5,11 +5,60 @@ export interface LegacyChange {
|
|
|
5
5
|
source: string;
|
|
6
6
|
}
|
|
7
7
|
export interface QuonfigFile {
|
|
8
|
-
contents
|
|
8
|
+
/** File contents when writing. Ignored when deleted is true. */
|
|
9
|
+
contents?: string;
|
|
10
|
+
/** When true, the file at `path` should be removed (tombstone). Default false. */
|
|
11
|
+
deleted?: boolean;
|
|
9
12
|
path: string;
|
|
10
13
|
}
|
|
14
|
+
export interface DroppedOverrideSummary {
|
|
15
|
+
/** Per-envId → per-output-path count of dropped override sections. */
|
|
16
|
+
byEnv: Record<string, Record<string, number>>;
|
|
17
|
+
/** Total override sections dropped across every env + flag. */
|
|
18
|
+
total: number;
|
|
19
|
+
}
|
|
20
|
+
export interface SkippedConfigEntry {
|
|
21
|
+
/** Source key (pre-translate) that was skipped. */
|
|
22
|
+
key: string;
|
|
23
|
+
/** Human-readable reason (e.g. variant/valueType mismatch). */
|
|
24
|
+
reason: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SkippedConfigSummary {
|
|
27
|
+
/** Individual skip records, in insertion order. */
|
|
28
|
+
entries: SkippedConfigEntry[];
|
|
29
|
+
/** Total configs skipped. */
|
|
30
|
+
total: number;
|
|
31
|
+
}
|
|
32
|
+
export interface DuplicateResolution {
|
|
33
|
+
/** All types that collided for this key (e.g. ['config', 'feature_flag']). */
|
|
34
|
+
collisionTypes: string[];
|
|
35
|
+
/** Paths that were deleted to resolve the collision. */
|
|
36
|
+
deleted: string[];
|
|
37
|
+
/** The path that was kept as the winning type. */
|
|
38
|
+
kept: string;
|
|
39
|
+
/** The qfg key that collided across types. */
|
|
40
|
+
key: string;
|
|
41
|
+
}
|
|
42
|
+
export interface DuplicateResolutionSummary {
|
|
43
|
+
entries: DuplicateResolution[];
|
|
44
|
+
total: number;
|
|
45
|
+
}
|
|
11
46
|
export interface MigrationSource {
|
|
12
47
|
fetchChanges(sinceEpochMs: null | number): AsyncIterable<LegacyChange>;
|
|
48
|
+
/**
|
|
49
|
+
* Optional post-translate accumulator. Returns any override sections that were
|
|
50
|
+
* dropped during translate() calls because the env.id was not present in the
|
|
51
|
+
* source's env map (e.g. archived/deleted Reforge envs). Returns null if nothing
|
|
52
|
+
* was dropped. Callers should consume + surface this before writing the migration
|
|
53
|
+
* report so customers can review the loss.
|
|
54
|
+
*/
|
|
55
|
+
getDroppedOverrides?(): DroppedOverrideSummary | null;
|
|
56
|
+
/**
|
|
57
|
+
* Optional post-translate accumulator. Returns any configs that translate() soft-
|
|
58
|
+
* skipped due to invalid source data (e.g. variant/valueType mismatch). Null when
|
|
59
|
+
* nothing was skipped.
|
|
60
|
+
*/
|
|
61
|
+
getSkippedConfigs?(): SkippedConfigSummary | null;
|
|
13
62
|
listEnvironments(): Promise<string[]>;
|
|
14
63
|
name: string;
|
|
15
64
|
translate(change: LegacyChange): QuonfigFile[];
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import type { LaunchChangeEntry, LaunchChangeGroup, LaunchConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by transformConfig when the source config is structurally invalid
|
|
4
|
+
* (e.g. variant/valueType mismatch) and the migrator cannot produce a valid
|
|
5
|
+
* qfg document from it. Callers in the source layer catch this specifically
|
|
6
|
+
* to soft-skip the config with a warning rather than aborting the whole run.
|
|
7
|
+
*/
|
|
8
|
+
export declare class InvalidSourceConfigError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
2
11
|
export declare function slugify(value: string): string;
|
|
3
12
|
export declare function isLegacyLogLevel(type: string): boolean;
|
|
4
|
-
export declare function
|
|
13
|
+
export declare function normalizeLogLevelKey(key: string): string;
|
|
14
|
+
export declare function transformConfig(config: LaunchConfig, envIdMap: Record<string, string>, onDroppedEnv?: (envId: string) => void): Record<string, unknown>;
|
|
5
15
|
export declare function getOutputPath(type: string, key: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Walks the set of written files looking for cross-type collisions (the same
|
|
18
|
+
* key appearing in multiple type-dirs). Reforge occasionally produces a config
|
|
19
|
+
* and a feature_flag with the same key simultaneously — qfg requires globally-
|
|
20
|
+
* unique keys, so we resolve the collision by keeping the config side and
|
|
21
|
+
* marking the other side(s) for deletion. Callers should fs.unlinkSync the
|
|
22
|
+
* paths in each resolution's `deleted` array and surface the resolutions in
|
|
23
|
+
* stderr + MIGRATION_REPORT.md so the customer can clean up the source data.
|
|
24
|
+
*
|
|
25
|
+
* When the collision does NOT include 'config' (an unexpected pattern we have
|
|
26
|
+
* not observed in production data), we still throw so the case surfaces for
|
|
27
|
+
* review rather than silently picking a tiebreak.
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectDuplicateKeys(files: Array<{
|
|
30
|
+
path: string;
|
|
31
|
+
}>): import('../../source.js').DuplicateResolution[];
|
|
6
32
|
export declare function groupChanges(changes: LaunchChangeEntry[]): LaunchChangeGroup[];
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import { zodToJsonSchema } from './zod-to-json-schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by transformConfig when the source config is structurally invalid
|
|
4
|
+
* (e.g. variant/valueType mismatch) and the migrator cannot produce a valid
|
|
5
|
+
* qfg document from it. Callers in the source layer catch this specifically
|
|
6
|
+
* to soft-skip the config with a warning rather than aborting the whole run.
|
|
7
|
+
*/
|
|
8
|
+
export class InvalidSourceConfigError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'InvalidSourceConfigError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
1
14
|
export function slugify(value) {
|
|
2
15
|
return value
|
|
3
16
|
.toLowerCase()
|
|
@@ -7,6 +20,9 @@ export function slugify(value) {
|
|
|
7
20
|
export function isLegacyLogLevel(type) {
|
|
8
21
|
return type.toUpperCase() === 'LOG_LEVEL';
|
|
9
22
|
}
|
|
23
|
+
export function normalizeLogLevelKey(key) {
|
|
24
|
+
return key.startsWith('log-level.') ? key : `log-level.${key}`;
|
|
25
|
+
}
|
|
10
26
|
function zeroValue(valueType) {
|
|
11
27
|
switch (valueType) {
|
|
12
28
|
case 'bool': {
|
|
@@ -106,7 +122,7 @@ function normalizeJsonValuesInConfig(out) {
|
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
124
|
}
|
|
109
|
-
export function transformConfig(config, envIdMap) {
|
|
125
|
+
export function transformConfig(config, envIdMap, onDroppedEnv) {
|
|
110
126
|
const out = JSON.parse(JSON.stringify(config));
|
|
111
127
|
delete out.changedBy;
|
|
112
128
|
delete out.modifiedAt;
|
|
@@ -121,24 +137,61 @@ export function transformConfig(config, envIdMap) {
|
|
|
121
137
|
if (typeof out.valueType === 'string' && out.valueType.toUpperCase() === 'LOG_LEVEL_V2') {
|
|
122
138
|
out.valueType = 'log_level';
|
|
123
139
|
}
|
|
140
|
+
if (out.type === 'log_level' && typeof out.key === 'string') {
|
|
141
|
+
out.key = normalizeLogLevelKey(out.key);
|
|
142
|
+
}
|
|
124
143
|
if (typeof out.type === 'string' && out.type.toLowerCase() === 'schema') {
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
const defaultSection = out.default;
|
|
145
|
+
const firstRule = defaultSection?.rules?.[0];
|
|
146
|
+
const innerValue = firstRule?.value?.value;
|
|
147
|
+
if (!innerValue || innerValue.schemaType !== 'ZOD' || typeof innerValue.schema !== 'string') {
|
|
148
|
+
throw new Error(`Unsupported schema payload for ${String(out.key ?? 'unknown')}`);
|
|
149
|
+
}
|
|
150
|
+
const key = String(out.key ?? 'unknown');
|
|
151
|
+
let converted;
|
|
152
|
+
try {
|
|
153
|
+
converted = zodToJsonSchema(innerValue.schema);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
157
|
+
throw new Error(`Schema conversion failed for "${key}": ${msg}\n--- Zod source ---\n${innerValue.schema}\n--- end ---`);
|
|
158
|
+
}
|
|
159
|
+
return converted.schema;
|
|
127
160
|
}
|
|
128
161
|
if (Array.isArray(out.environments)) {
|
|
162
|
+
const kept = [];
|
|
129
163
|
for (const env of out.environments) {
|
|
130
164
|
if (typeof env.id === 'string') {
|
|
131
|
-
const name = envIdMap[env.id]
|
|
165
|
+
const name = envIdMap[env.id];
|
|
166
|
+
if (name === undefined) {
|
|
167
|
+
onDroppedEnv?.(env.id);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
132
170
|
env.id = slugify(name);
|
|
133
171
|
}
|
|
172
|
+
kept.push(env);
|
|
173
|
+
}
|
|
174
|
+
out.environments = kept;
|
|
175
|
+
}
|
|
176
|
+
if (typeof out.valueType === 'string' &&
|
|
177
|
+
Array.isArray(out.variants) &&
|
|
178
|
+
!['provided', 'schema', 'weighted_values'].includes(out.valueType)) {
|
|
179
|
+
const configKey = String(out.key ?? 'unknown');
|
|
180
|
+
for (const [i, variant] of out.variants.entries()) {
|
|
181
|
+
const v = variant?.value;
|
|
182
|
+
if (v && typeof v.type === 'string' && v.type !== out.valueType) {
|
|
183
|
+
throw new InvalidSourceConfigError(`Variant type mismatch for "${configKey}" at variants[${i}]: value type "${v.type}" does not match config valueType "${String(out.valueType)}"`);
|
|
184
|
+
}
|
|
134
185
|
}
|
|
135
186
|
}
|
|
136
187
|
if (!out.default && typeof out.valueType === 'string') {
|
|
188
|
+
const variants = Array.isArray(out.variants) ? out.variants : [];
|
|
189
|
+
const firstVariantValue = variants.length > 0 && variants[0] && typeof variants[0] === 'object' ? variants[0].value : undefined;
|
|
137
190
|
out.default = {
|
|
138
191
|
rules: [
|
|
139
192
|
{
|
|
140
193
|
criteria: [{ operator: 'ALWAYS_TRUE' }],
|
|
141
|
-
value: zeroValue(out.valueType),
|
|
194
|
+
value: firstVariantValue ?? zeroValue(out.valueType),
|
|
142
195
|
},
|
|
143
196
|
],
|
|
144
197
|
};
|
|
@@ -153,7 +206,7 @@ export function getOutputPath(type, key) {
|
|
|
153
206
|
return `feature-flags/${key}.json`;
|
|
154
207
|
}
|
|
155
208
|
case 'LOG_LEVEL_V2': {
|
|
156
|
-
return `log-levels/${key}.json`;
|
|
209
|
+
return `log-levels/${normalizeLogLevelKey(key)}.json`;
|
|
157
210
|
}
|
|
158
211
|
case 'SCHEMA': {
|
|
159
212
|
return `schemas/${key}.json`;
|
|
@@ -166,6 +219,70 @@ export function getOutputPath(type, key) {
|
|
|
166
219
|
}
|
|
167
220
|
}
|
|
168
221
|
}
|
|
222
|
+
const PATH_DIR_TO_TYPE = {
|
|
223
|
+
configs: 'config',
|
|
224
|
+
'feature-flags': 'feature_flag',
|
|
225
|
+
'log-levels': 'log_level',
|
|
226
|
+
schemas: 'schema',
|
|
227
|
+
segments: 'segment',
|
|
228
|
+
};
|
|
229
|
+
const TYPE_TO_DIR = Object.fromEntries(Object.entries(PATH_DIR_TO_TYPE).map(([dir, type]) => [type, dir]));
|
|
230
|
+
/**
|
|
231
|
+
* Walks the set of written files looking for cross-type collisions (the same
|
|
232
|
+
* key appearing in multiple type-dirs). Reforge occasionally produces a config
|
|
233
|
+
* and a feature_flag with the same key simultaneously — qfg requires globally-
|
|
234
|
+
* unique keys, so we resolve the collision by keeping the config side and
|
|
235
|
+
* marking the other side(s) for deletion. Callers should fs.unlinkSync the
|
|
236
|
+
* paths in each resolution's `deleted` array and surface the resolutions in
|
|
237
|
+
* stderr + MIGRATION_REPORT.md so the customer can clean up the source data.
|
|
238
|
+
*
|
|
239
|
+
* When the collision does NOT include 'config' (an unexpected pattern we have
|
|
240
|
+
* not observed in production data), we still throw so the case surfaces for
|
|
241
|
+
* review rather than silently picking a tiebreak.
|
|
242
|
+
*/
|
|
243
|
+
export function detectDuplicateKeys(files) {
|
|
244
|
+
const keyToTypes = new Map();
|
|
245
|
+
for (const { path } of files) {
|
|
246
|
+
const firstSlash = path.indexOf('/');
|
|
247
|
+
if (firstSlash < 0)
|
|
248
|
+
continue;
|
|
249
|
+
const dir = path.slice(0, firstSlash);
|
|
250
|
+
const type = PATH_DIR_TO_TYPE[dir];
|
|
251
|
+
if (!type)
|
|
252
|
+
continue;
|
|
253
|
+
const file = path.slice(firstSlash + 1);
|
|
254
|
+
const key = file.replace(/\.json$/, '');
|
|
255
|
+
const set = keyToTypes.get(key) ?? new Set();
|
|
256
|
+
set.add(type);
|
|
257
|
+
keyToTypes.set(key, set);
|
|
258
|
+
}
|
|
259
|
+
const resolutions = [];
|
|
260
|
+
const unexpected = [];
|
|
261
|
+
for (const [key, types] of [...keyToTypes.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
262
|
+
if (types.size <= 1)
|
|
263
|
+
continue;
|
|
264
|
+
if (!types.has('config')) {
|
|
265
|
+
unexpected.push(`"${key}" (types: ${[...types].sort().join(', ')})`);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const kept = `configs/${key}.json`;
|
|
269
|
+
const deleted = [];
|
|
270
|
+
for (const type of [...types].sort()) {
|
|
271
|
+
if (type === 'config')
|
|
272
|
+
continue;
|
|
273
|
+
const dir = TYPE_TO_DIR[type];
|
|
274
|
+
if (!dir)
|
|
275
|
+
continue;
|
|
276
|
+
deleted.push(`${dir}/${key}.json`);
|
|
277
|
+
}
|
|
278
|
+
resolutions.push({ collisionTypes: [...types].sort(), deleted, kept, key });
|
|
279
|
+
}
|
|
280
|
+
if (unexpected.length > 0) {
|
|
281
|
+
throw new Error(`Unexpected cross-type key collision — no 'config' side to tiebreak on. ` +
|
|
282
|
+
`Please file a bead with the source data details:\n ${unexpected.join('\n ')}`);
|
|
283
|
+
}
|
|
284
|
+
return resolutions;
|
|
285
|
+
}
|
|
169
286
|
export function groupChanges(changes) {
|
|
170
287
|
const groups = new Map();
|
|
171
288
|
for (const change of changes) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type JsonSchema = boolean | JsonSchemaObject;
|
|
2
|
+
export interface JsonSchemaObject {
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
export interface ZodToJsonSchemaResult {
|
|
6
|
+
schema: JsonSchema;
|
|
7
|
+
warnings: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function zodToJsonSchema(source: string): ZodToJsonSchemaResult;
|