@ontrails/config 1.0.0-beta.12

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.
Files changed (159) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-lint.log +3 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/CHANGELOG.md +19 -0
  5. package/dist/app-config.d.ts +65 -0
  6. package/dist/app-config.d.ts.map +1 -0
  7. package/dist/app-config.js +172 -0
  8. package/dist/app-config.js.map +1 -0
  9. package/dist/collect.d.ts +11 -0
  10. package/dist/collect.d.ts.map +1 -0
  11. package/dist/collect.js +81 -0
  12. package/dist/collect.js.map +1 -0
  13. package/dist/compose.d.ts +26 -0
  14. package/dist/compose.d.ts.map +1 -0
  15. package/dist/compose.js +19 -0
  16. package/dist/compose.js.map +1 -0
  17. package/dist/config-layer.d.ts +11 -0
  18. package/dist/config-layer.d.ts.map +1 -0
  19. package/dist/config-layer.js +6 -0
  20. package/dist/config-layer.js.map +1 -0
  21. package/dist/config-service.d.ts +3 -0
  22. package/dist/config-service.d.ts.map +1 -0
  23. package/dist/config-service.js +26 -0
  24. package/dist/config-service.js.map +1 -0
  25. package/dist/define-config.d.ts +61 -0
  26. package/dist/define-config.d.ts.map +1 -0
  27. package/dist/define-config.js +90 -0
  28. package/dist/define-config.js.map +1 -0
  29. package/dist/describe.d.ts +25 -0
  30. package/dist/describe.d.ts.map +1 -0
  31. package/dist/describe.js +147 -0
  32. package/dist/describe.js.map +1 -0
  33. package/dist/doctor.d.ts +27 -0
  34. package/dist/doctor.d.ts.map +1 -0
  35. package/dist/doctor.js +167 -0
  36. package/dist/doctor.js.map +1 -0
  37. package/dist/explain.d.ts +30 -0
  38. package/dist/explain.d.ts.map +1 -0
  39. package/dist/explain.js +114 -0
  40. package/dist/explain.js.map +1 -0
  41. package/dist/extensions.d.ts +38 -0
  42. package/dist/extensions.d.ts.map +1 -0
  43. package/dist/extensions.js +35 -0
  44. package/dist/extensions.js.map +1 -0
  45. package/dist/generate/env.d.ts +15 -0
  46. package/dist/generate/env.d.ts.map +1 -0
  47. package/dist/generate/env.js +65 -0
  48. package/dist/generate/env.js.map +1 -0
  49. package/dist/generate/example.d.ts +16 -0
  50. package/dist/generate/example.d.ts.map +1 -0
  51. package/dist/generate/example.js +136 -0
  52. package/dist/generate/example.js.map +1 -0
  53. package/dist/generate/helpers.d.ts +35 -0
  54. package/dist/generate/helpers.d.ts.map +1 -0
  55. package/dist/generate/helpers.js +116 -0
  56. package/dist/generate/helpers.js.map +1 -0
  57. package/dist/generate/index.d.ts +4 -0
  58. package/dist/generate/index.d.ts.map +1 -0
  59. package/dist/generate/index.js +4 -0
  60. package/dist/generate/index.js.map +1 -0
  61. package/dist/generate/json-schema.d.ts +18 -0
  62. package/dist/generate/json-schema.d.ts.map +1 -0
  63. package/dist/generate/json-schema.js +97 -0
  64. package/dist/generate/json-schema.js.map +1 -0
  65. package/dist/index.d.ts +21 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +21 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/merge.d.ts +16 -0
  70. package/dist/merge.d.ts.map +1 -0
  71. package/dist/merge.js +34 -0
  72. package/dist/merge.js.map +1 -0
  73. package/dist/ref.d.ts +24 -0
  74. package/dist/ref.d.ts.map +1 -0
  75. package/dist/ref.js +25 -0
  76. package/dist/ref.js.map +1 -0
  77. package/dist/registry.d.ts +24 -0
  78. package/dist/registry.d.ts.map +1 -0
  79. package/dist/registry.js +12 -0
  80. package/dist/registry.js.map +1 -0
  81. package/dist/resolve.d.ts +21 -0
  82. package/dist/resolve.d.ts.map +1 -0
  83. package/dist/resolve.js +174 -0
  84. package/dist/resolve.js.map +1 -0
  85. package/dist/secret-heuristics.d.ts +10 -0
  86. package/dist/secret-heuristics.d.ts.map +1 -0
  87. package/dist/secret-heuristics.js +11 -0
  88. package/dist/secret-heuristics.js.map +1 -0
  89. package/dist/trails/config-check.d.ts +11 -0
  90. package/dist/trails/config-check.d.ts.map +1 -0
  91. package/dist/trails/config-check.js +53 -0
  92. package/dist/trails/config-check.js.map +1 -0
  93. package/dist/trails/config-describe.d.ts +12 -0
  94. package/dist/trails/config-describe.d.ts.map +1 -0
  95. package/dist/trails/config-describe.js +41 -0
  96. package/dist/trails/config-describe.js.map +1 -0
  97. package/dist/trails/config-explain.d.ts +8 -0
  98. package/dist/trails/config-explain.d.ts.map +1 -0
  99. package/dist/trails/config-explain.js +74 -0
  100. package/dist/trails/config-explain.js.map +1 -0
  101. package/dist/trails/config-init.d.ts +9 -0
  102. package/dist/trails/config-init.d.ts.map +1 -0
  103. package/dist/trails/config-init.js +78 -0
  104. package/dist/trails/config-init.js.map +1 -0
  105. package/dist/workspace.d.ts +9 -0
  106. package/dist/workspace.d.ts.map +1 -0
  107. package/dist/workspace.js +44 -0
  108. package/dist/workspace.js.map +1 -0
  109. package/dist/zod-utils.d.ts +14 -0
  110. package/dist/zod-utils.d.ts.map +1 -0
  111. package/dist/zod-utils.js +41 -0
  112. package/dist/zod-utils.js.map +1 -0
  113. package/package.json +20 -0
  114. package/src/__tests__/app-config.test.ts +329 -0
  115. package/src/__tests__/compose.test.ts +59 -0
  116. package/src/__tests__/config-check.test.ts +171 -0
  117. package/src/__tests__/config-describe.test.ts +154 -0
  118. package/src/__tests__/config-explain.test.ts +167 -0
  119. package/src/__tests__/config-init.test.ts +210 -0
  120. package/src/__tests__/config-layer.test.ts +53 -0
  121. package/src/__tests__/config-service.test.ts +87 -0
  122. package/src/__tests__/define-config.test.ts +263 -0
  123. package/src/__tests__/describe.test.ts +158 -0
  124. package/src/__tests__/doctor.test.ts +172 -0
  125. package/src/__tests__/explain.test.ts +139 -0
  126. package/src/__tests__/extensions.test.ts +134 -0
  127. package/src/__tests__/generate.test.ts +269 -0
  128. package/src/__tests__/ref.test.ts +35 -0
  129. package/src/__tests__/resolve.test.ts +246 -0
  130. package/src/__tests__/workspace.test.ts +64 -0
  131. package/src/app-config.ts +307 -0
  132. package/src/collect.ts +118 -0
  133. package/src/compose.ts +46 -0
  134. package/src/config-layer.ts +15 -0
  135. package/src/config-service.ts +32 -0
  136. package/src/define-config.ts +134 -0
  137. package/src/describe.ts +252 -0
  138. package/src/doctor.ts +219 -0
  139. package/src/explain.ts +176 -0
  140. package/src/extensions.ts +51 -0
  141. package/src/generate/env.ts +104 -0
  142. package/src/generate/example.ts +222 -0
  143. package/src/generate/helpers.ts +158 -0
  144. package/src/generate/index.ts +3 -0
  145. package/src/generate/json-schema.ts +137 -0
  146. package/src/index.ts +44 -0
  147. package/src/merge.ts +43 -0
  148. package/src/ref.ts +38 -0
  149. package/src/registry.ts +33 -0
  150. package/src/resolve.ts +279 -0
  151. package/src/secret-heuristics.ts +13 -0
  152. package/src/trails/config-check.ts +60 -0
  153. package/src/trails/config-describe.ts +44 -0
  154. package/src/trails/config-explain.ts +93 -0
  155. package/src/trails/config-init.ts +96 -0
  156. package/src/workspace.ts +51 -0
  157. package/src/zod-utils.ts +53 -0
  158. package/tsconfig.json +9 -0
  159. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `.env.example` file generation from Zod schema `env()` bindings.
3
+ *
4
+ * Lists each env var with its type, default, and whether it is a secret.
5
+ * Returns an empty string when no env bindings are present.
6
+ */
7
+ import type { z } from 'zod';
8
+
9
+ import { collectConfigMeta } from '../collect.js';
10
+ import type { ConfigFieldMeta } from '../extensions.js';
11
+
12
+ import { isLikelySecret } from '../secret-heuristics.js';
13
+
14
+ import {
15
+ formatValue,
16
+ getDefault,
17
+ getDescription,
18
+ resolveFieldByPath,
19
+ unwrap,
20
+ zodTypeName,
21
+ zodTypeToJsonSchema,
22
+ } from './helpers.js';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Map Zod type names to human-readable type labels. */
29
+ const typeLabel = (schema: z.ZodType): string => {
30
+ const typeName = zodTypeName(unwrap(schema));
31
+ return zodTypeToJsonSchema[typeName] ?? typeName;
32
+ };
33
+
34
+ /** Build the type annotation comment for an env entry. */
35
+ const envTypeAnnotation = (
36
+ fieldSchema: z.ZodType,
37
+ meta: ConfigFieldMeta
38
+ ): string => {
39
+ const parts = [`type: ${typeLabel(fieldSchema)}`];
40
+ const info = getDefault(fieldSchema);
41
+ if (info.has) {
42
+ parts.push(`default: ${formatValue(info.value)}`);
43
+ }
44
+ if (meta.secret) {
45
+ parts.push('secret');
46
+ }
47
+ return `# ${parts.join(', ')}`;
48
+ };
49
+
50
+ /** Format a single env var entry. */
51
+ const envEntry = (
52
+ envVar: string,
53
+ fieldSchema: z.ZodType,
54
+ meta: ConfigFieldMeta
55
+ ): readonly string[] => {
56
+ const effectiveMeta =
57
+ !meta.secret && isLikelySecret(envVar) ? { ...meta, secret: true } : meta;
58
+
59
+ const lines: string[] = [];
60
+ const desc = getDescription(fieldSchema);
61
+ if (desc) {
62
+ lines.push(`# ${desc}`);
63
+ }
64
+ lines.push(envTypeAnnotation(fieldSchema, effectiveMeta));
65
+ lines.push(`${envVar}=`);
66
+ return lines;
67
+ };
68
+
69
+ /** Collect env entries from config metadata. */
70
+ const collectEnvEntries = (
71
+ schema: z.ZodObject<Record<string, z.ZodType>>,
72
+ meta: Map<string, ConfigFieldMeta>
73
+ ): readonly string[] => {
74
+ const entries: string[] = [];
75
+ for (const [path, fieldMeta] of meta) {
76
+ if (!fieldMeta.env) {
77
+ continue;
78
+ }
79
+ const fieldSchema = resolveFieldByPath(schema, path);
80
+ if (!fieldSchema) {
81
+ continue;
82
+ }
83
+ entries.push(...envEntry(fieldMeta.env, fieldSchema, fieldMeta));
84
+ entries.push('');
85
+ }
86
+ return entries;
87
+ };
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Public API
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Generate a `.env.example` file from `env()` bindings in the schema.
95
+ *
96
+ * Lists each env var with its type, default, and whether it is a secret.
97
+ * Returns an empty string when no env bindings are present.
98
+ */
99
+ export const generateEnvExample = (
100
+ schema: z.ZodObject<Record<string, z.ZodType>>
101
+ ): string => {
102
+ const entries = collectEnvEntries(schema, collectConfigMeta(schema));
103
+ return entries.length === 0 ? '' : entries.join('\n');
104
+ };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Config example file generation in multiple formats.
3
+ *
4
+ * Produces TOML, JSON, JSONC, and YAML example files from a Zod schema,
5
+ * with defaults shown and deprecated fields annotated.
6
+ */
7
+ import type { z } from 'zod';
8
+
9
+ import {
10
+ fieldComments,
11
+ fieldValue,
12
+ formatValue,
13
+ getObjectShape,
14
+ isObjectType,
15
+ pushComments,
16
+ unwrap,
17
+ } from './helpers.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // TOML generation
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /** Render a flat set of fields as TOML key = value lines. */
24
+ const renderTomlFields = (
25
+ shape: Record<string, z.ZodType>,
26
+ lines: string[]
27
+ ): void => {
28
+ for (const [key, fieldSchema] of Object.entries(shape)) {
29
+ if (isObjectType(fieldSchema)) {
30
+ continue;
31
+ }
32
+ pushComments(lines, fieldComments(fieldSchema, '#'), '');
33
+ lines.push(`${key} = ${formatValue(fieldValue(fieldSchema))}`);
34
+ }
35
+ };
36
+
37
+ /** Render nested objects as TOML sections, recursing with dotted prefixes. */
38
+ const renderTomlSections = (
39
+ shape: Record<string, z.ZodType>,
40
+ lines: string[],
41
+ prefix = ''
42
+ ): void => {
43
+ for (const [key, fieldSchema] of Object.entries(shape)) {
44
+ const nested = isObjectType(fieldSchema)
45
+ ? getObjectShape(fieldSchema)
46
+ : undefined;
47
+ if (!nested) {
48
+ continue;
49
+ }
50
+ const sectionKey = prefix ? `${prefix}.${key}` : key;
51
+ if (lines.length > 0) {
52
+ lines.push('');
53
+ }
54
+ lines.push(`[${sectionKey}]`);
55
+ renderTomlFields(nested, lines);
56
+ // oxlint-disable-next-line max-statements -- recursive TOML section rendering
57
+ renderTomlSections(nested, lines, sectionKey);
58
+ }
59
+ };
60
+
61
+ const formatToml = (schema: z.ZodObject<Record<string, z.ZodType>>): string => {
62
+ const shape = schema.shape as Record<string, z.ZodType>;
63
+ const lines: string[] = [];
64
+ renderTomlFields(shape, lines);
65
+ renderTomlSections(shape, lines);
66
+ return `${lines.join('\n')}\n`;
67
+ };
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // JSON / JSONC generation
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /** Resolve a field to its nested object or its scalar value. */
74
+ const resolveJsonField = (
75
+ fieldSchema: z.ZodType,
76
+ recurse: (
77
+ s: z.ZodObject<Record<string, z.ZodType>>
78
+ ) => Record<string, unknown>
79
+ ): unknown => {
80
+ if (!isObjectType(fieldSchema)) {
81
+ return fieldValue(fieldSchema);
82
+ }
83
+ const nested = getObjectShape(fieldSchema);
84
+ if (!nested) {
85
+ return fieldValue(fieldSchema);
86
+ }
87
+ const inner = unwrap(fieldSchema) as z.ZodObject<Record<string, z.ZodType>>;
88
+ return recurse(inner);
89
+ };
90
+
91
+ /** Build a plain object with default/placeholder values for JSON output. */
92
+ const buildJsonObject = (
93
+ schema: z.ZodObject<Record<string, z.ZodType>>
94
+ ): Record<string, unknown> => {
95
+ const shape = schema.shape as Record<string, z.ZodType>;
96
+ const obj: Record<string, unknown> = {};
97
+ for (const [key, fieldSchema] of Object.entries(shape)) {
98
+ obj[key] = resolveJsonField(fieldSchema, buildJsonObject);
99
+ }
100
+ return obj;
101
+ };
102
+
103
+ const formatJson = (schema: z.ZodObject<Record<string, z.ZodType>>): string =>
104
+ `${JSON.stringify(buildJsonObject(schema), null, 2)}\n`;
105
+
106
+ /** Serialize a nested object field for JSONC output. */
107
+ const serializeJsoncObject = (fieldSchema: z.ZodType): string | undefined => {
108
+ const nested = getObjectShape(fieldSchema);
109
+ if (!nested) {
110
+ return undefined;
111
+ }
112
+ const inner = unwrap(fieldSchema) as z.ZodObject<Record<string, z.ZodType>>;
113
+ return JSON.stringify(buildJsonObject(inner), null, 2).replaceAll(
114
+ '\n',
115
+ '\n '
116
+ );
117
+ };
118
+
119
+ /** Render a single JSONC entry (comments + key: value). */
120
+ const renderJsoncEntry = (
121
+ key: string,
122
+ fieldSchema: z.ZodType,
123
+ isLast: boolean,
124
+ lines: string[]
125
+ ): void => {
126
+ pushComments(lines, fieldComments(fieldSchema, '//'), ' ');
127
+ const comma = isLast ? '' : ',';
128
+ const serialized = isObjectType(fieldSchema)
129
+ ? serializeJsoncObject(fieldSchema)
130
+ : undefined;
131
+ const val = serialized ?? formatValue(fieldValue(fieldSchema));
132
+ lines.push(` "${key}": ${val}${comma}`);
133
+ };
134
+
135
+ const formatJsonc = (
136
+ schema: z.ZodObject<Record<string, z.ZodType>>
137
+ ): string => {
138
+ const keys = Object.keys(schema.shape);
139
+ const shape = schema.shape as Record<string, z.ZodType>;
140
+ const lines: string[] = ['{'];
141
+
142
+ for (let i = 0; i < keys.length; i += 1) {
143
+ const key = keys[i] as string;
144
+ renderJsoncEntry(
145
+ key,
146
+ shape[key] as z.ZodType,
147
+ i === keys.length - 1,
148
+ lines
149
+ );
150
+ }
151
+
152
+ lines.push('}');
153
+ return `${lines.join('\n')}\n`;
154
+ };
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // YAML generation
158
+ // ---------------------------------------------------------------------------
159
+
160
+ /** Render a single YAML scalar field. */
161
+ const renderYamlScalar = (
162
+ key: string,
163
+ fieldSchema: z.ZodType,
164
+ indent: string,
165
+ lines: string[]
166
+ ): void => {
167
+ pushComments(lines, fieldComments(fieldSchema, '#'), indent);
168
+ lines.push(`${indent}${key}: ${formatValue(fieldValue(fieldSchema))}`);
169
+ };
170
+
171
+ /** Render YAML fields at a given indent level. */
172
+ const renderYamlFields = (
173
+ shape: Record<string, z.ZodType>,
174
+ indent: string,
175
+ lines: string[]
176
+ ): void => {
177
+ for (const [key, fieldSchema] of Object.entries(shape)) {
178
+ if (isObjectType(fieldSchema)) {
179
+ const nested = getObjectShape(fieldSchema);
180
+ if (!nested) {
181
+ continue;
182
+ }
183
+ lines.push(`${indent}${key}:`);
184
+ renderYamlFields(nested, `${indent} `, lines);
185
+ } else {
186
+ renderYamlScalar(key, fieldSchema, indent, lines);
187
+ }
188
+ }
189
+ };
190
+
191
+ const formatYaml = (schema: z.ZodObject<Record<string, z.ZodType>>): string => {
192
+ const shape = schema.shape as Record<string, z.ZodType>;
193
+ const lines: string[] = [];
194
+ renderYamlFields(shape, '', lines);
195
+ return `${lines.join('\n')}\n`;
196
+ };
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Format dispatch
200
+ // ---------------------------------------------------------------------------
201
+
202
+ type ExampleFormat = 'json' | 'jsonc' | 'toml' | 'yaml';
203
+
204
+ const formatters: Record<
205
+ ExampleFormat,
206
+ (schema: z.ZodObject<Record<string, z.ZodType>>) => string
207
+ > = {
208
+ json: formatJson,
209
+ jsonc: formatJsonc,
210
+ toml: formatToml,
211
+ yaml: formatYaml,
212
+ };
213
+
214
+ /**
215
+ * Generate an example config file in the specified format.
216
+ *
217
+ * Includes descriptions as comments, defaults shown, deprecated fields annotated.
218
+ */
219
+ export const generateExample = (
220
+ schema: z.ZodObject<Record<string, z.ZodType>>,
221
+ format: ExampleFormat
222
+ ): string => formatters[format](schema);
@@ -0,0 +1,158 @@
1
+ import { globalRegistry } from 'zod';
2
+ import type { z } from 'zod';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Schema introspection helpers
6
+ // ---------------------------------------------------------------------------
7
+
8
+ /** Unwrap `.default()`, `.optional()`, `.nullable()` to reach the inner type. */
9
+ export const unwrap = (schema: z.ZodType): z.ZodType => {
10
+ let current = schema;
11
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
12
+ while (true) {
13
+ const def = current.def as unknown as Record<string, unknown>;
14
+ const inner = def['innerType'] as z.ZodType | undefined;
15
+ if (!inner) {
16
+ return current;
17
+ }
18
+ current = inner;
19
+ }
20
+ };
21
+
22
+ /** Read the Zod type discriminant from a schema's def. */
23
+ export const zodTypeName = (schema: z.ZodType): string => {
24
+ const def = schema.def as unknown as Record<string, unknown>;
25
+ return (def['type'] as string) ?? 'unknown';
26
+ };
27
+
28
+ /** Walk the registry up through wrappers to find a metadata key. */
29
+ export const walkRegistryKey = (
30
+ schema: z.ZodType,
31
+ key: string
32
+ ): unknown | undefined => {
33
+ let current: z.ZodType | undefined = schema;
34
+ while (current) {
35
+ const meta = globalRegistry.get(current) as
36
+ | Record<string, unknown>
37
+ | undefined;
38
+ if (meta?.[key] !== undefined) {
39
+ return meta[key];
40
+ }
41
+ const def = current.def as unknown as Record<string, unknown>;
42
+ current = def['innerType'] as z.ZodType | undefined;
43
+ }
44
+ return undefined;
45
+ };
46
+
47
+ /** Get the description from the global registry for a schema or its inner type. */
48
+ export const getDescription = (schema: z.ZodType): string | undefined =>
49
+ walkRegistryKey(schema, 'description') as string | undefined;
50
+
51
+ /** Get the deprecation message from the config meta for a field. */
52
+ export const getDeprecation = (schema: z.ZodType): string | undefined =>
53
+ walkRegistryKey(schema, 'deprecationMessage') as string | undefined;
54
+
55
+ /** Get the default value from a schema if it has one. */
56
+ export const getDefault = (
57
+ schema: z.ZodType
58
+ ): { has: false } | { has: true; value: unknown } => {
59
+ const def = schema.def as unknown as Record<string, unknown>;
60
+ if (def['type'] === 'default') {
61
+ return { has: true, value: def['defaultValue'] };
62
+ }
63
+ return { has: false };
64
+ };
65
+
66
+ /** Check whether a schema (or its inner type) is a ZodObject. */
67
+ export const isObjectType = (schema: z.ZodType): boolean => {
68
+ const inner = unwrap(schema);
69
+ const def = inner.def as unknown as Record<string, unknown>;
70
+ return def['type'] === 'object' && 'shape' in def;
71
+ };
72
+
73
+ /** Get the shape of a ZodObject, unwrapping wrappers first. */
74
+ export const getObjectShape = (
75
+ schema: z.ZodType
76
+ ): Record<string, z.ZodType> | undefined => {
77
+ const inner = unwrap(schema);
78
+ const def = inner.def as unknown as Record<string, unknown>;
79
+ if (def['type'] !== 'object' || !('shape' in def)) {
80
+ return undefined;
81
+ }
82
+ return (inner as z.ZodObject<Record<string, z.ZodType>>).shape as Record<
83
+ string,
84
+ z.ZodType
85
+ >;
86
+ };
87
+
88
+ /** Resolve a dot-separated path to the field schema within an object. */
89
+ export const resolveFieldByPath = (
90
+ schema: z.ZodObject<Record<string, z.ZodType>>,
91
+ path: string
92
+ ): z.ZodType | undefined => {
93
+ const parts = path.split('.');
94
+ let current: z.ZodType = schema;
95
+
96
+ for (const part of parts) {
97
+ const shape = getObjectShape(current);
98
+ if (!shape?.[part]) {
99
+ return undefined;
100
+ }
101
+ current = shape[part];
102
+ }
103
+
104
+ return current;
105
+ };
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Value formatting helpers
109
+ // ---------------------------------------------------------------------------
110
+
111
+ /** Format a value as a quoted string or literal. */
112
+ export const formatValue = (value: unknown): string => {
113
+ if (typeof value === 'string') {
114
+ return `"${value}"`;
115
+ }
116
+ return String(value);
117
+ };
118
+
119
+ /** Get the display value for a field (default or empty placeholder). */
120
+ export const fieldValue = (schema: z.ZodType): unknown => {
121
+ const info = getDefault(schema);
122
+ return info.has ? info.value : '';
123
+ };
124
+
125
+ /** Collect comment lines for a field (description + deprecation). */
126
+ export const fieldComments = (
127
+ schema: z.ZodType,
128
+ prefix: string
129
+ ): readonly string[] => {
130
+ const lines: string[] = [];
131
+ const desc = getDescription(schema);
132
+ if (desc) {
133
+ lines.push(`${prefix} ${desc}`);
134
+ }
135
+ const dep = getDeprecation(schema);
136
+ if (dep) {
137
+ lines.push(`${prefix} DEPRECATED: ${dep}`);
138
+ }
139
+ return lines;
140
+ };
141
+
142
+ /** Append comment lines to an output array. */
143
+ export const pushComments = (
144
+ lines: string[],
145
+ comments: readonly string[],
146
+ indent: string
147
+ ): void => {
148
+ for (const c of comments) {
149
+ lines.push(`${indent}${c}`);
150
+ }
151
+ };
152
+
153
+ /** Map a Zod type name to a JSON Schema type. */
154
+ export const zodTypeToJsonSchema: Record<string, string> = {
155
+ boolean: 'boolean',
156
+ number: 'number',
157
+ string: 'string',
158
+ };
@@ -0,0 +1,3 @@
1
+ export { generateEnvExample } from './env.js';
2
+ export { generateExample } from './example.js';
3
+ export { generateJsonSchema } from './json-schema.js';
@@ -0,0 +1,137 @@
1
+ /**
2
+ * JSON Schema generation from Zod object schemas.
3
+ *
4
+ * Produces JSON Schema Draft 2020-12 with descriptions, defaults,
5
+ * deprecated annotations, and constraints.
6
+ */
7
+ import type { z } from 'zod';
8
+
9
+ import {
10
+ getDefault,
11
+ getDeprecation,
12
+ getDescription,
13
+ getObjectShape,
14
+ isObjectType,
15
+ unwrap,
16
+ zodTypeName,
17
+ zodTypeToJsonSchema,
18
+ } from './helpers.js';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /** Extract the JSON Schema type or enum from a Zod schema. */
25
+ const jsonSchemaType = (inner: z.ZodType): Record<string, unknown> => {
26
+ const typeName = zodTypeName(inner);
27
+ if (typeName === 'enum') {
28
+ const def = inner.def as unknown as Record<string, unknown>;
29
+ const entries = def['entries'] as Record<string, string> | undefined;
30
+ return entries ? { enum: Object.keys(entries) } : {};
31
+ }
32
+ const mapped = zodTypeToJsonSchema[typeName];
33
+ return mapped ? { type: mapped } : {};
34
+ };
35
+
36
+ /** Annotation extractors for JSON Schema properties. */
37
+ const annotationExtractors: readonly ((
38
+ s: z.ZodType
39
+ ) => Record<string, unknown>)[] = [
40
+ (s) => {
41
+ const desc = getDescription(s);
42
+ return desc ? { description: desc } : {};
43
+ },
44
+ (s) => {
45
+ const info = getDefault(s);
46
+ return info.has ? { default: info.value } : {};
47
+ },
48
+ (s) => {
49
+ const dep = getDeprecation(s);
50
+ return dep ? { deprecated: true } : {};
51
+ },
52
+ ];
53
+
54
+ /** Extract annotations (description, default, deprecated) for JSON Schema. */
55
+ const jsonSchemaAnnotations = (
56
+ fieldSchema: z.ZodType
57
+ ): Record<string, unknown> =>
58
+ Object.assign({}, ...annotationExtractors.map((fn) => fn(fieldSchema)));
59
+
60
+ /** Check if a field is required (no default and not optional). */
61
+ const isRequired = (fieldSchema: z.ZodType): boolean => {
62
+ const def = fieldSchema.def as unknown as Record<string, unknown>;
63
+ return def['type'] !== 'default' && def['type'] !== 'optional';
64
+ };
65
+
66
+ /** Convert a single Zod field schema to a JSON Schema property. */
67
+ const fieldToJsonSchema = (fieldSchema: z.ZodType): Record<string, unknown> => {
68
+ if (isObjectType(fieldSchema)) {
69
+ const nestedShape = getObjectShape(fieldSchema);
70
+ if (nestedShape) {
71
+ // oxlint-disable-next-line no-use-before-define -- mutual recursion with buildSchemaProperties
72
+ const { properties, required } = buildSchemaProperties(nestedShape);
73
+ return {
74
+ properties,
75
+ type: 'object',
76
+ ...(required.length > 0 ? { required } : {}),
77
+ ...jsonSchemaAnnotations(fieldSchema),
78
+ };
79
+ }
80
+ }
81
+ return {
82
+ ...jsonSchemaType(unwrap(fieldSchema)),
83
+ ...jsonSchemaAnnotations(fieldSchema),
84
+ };
85
+ };
86
+
87
+ /** Build properties and required arrays from a schema shape. */
88
+ const buildSchemaProperties = (
89
+ shape: Record<string, z.ZodType>
90
+ ): {
91
+ properties: Record<string, Record<string, unknown>>;
92
+ required: readonly string[];
93
+ } => {
94
+ const properties: Record<string, Record<string, unknown>> = {};
95
+ const required: string[] = [];
96
+ for (const [key, fieldSchema] of Object.entries(shape)) {
97
+ properties[key] = fieldToJsonSchema(fieldSchema);
98
+ if (isRequired(fieldSchema)) {
99
+ required.push(key);
100
+ }
101
+ }
102
+ return { properties, required };
103
+ };
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Public API
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Generate a JSON Schema from a Zod object schema.
111
+ *
112
+ * Includes descriptions, defaults, deprecated annotations, and constraints.
113
+ * Produces JSON Schema Draft 2020-12.
114
+ */
115
+ export const generateJsonSchema = (
116
+ schema: z.ZodObject<Record<string, z.ZodType>>,
117
+ options?: { readonly description?: string; readonly title?: string }
118
+ ): Record<string, unknown> => {
119
+ const { properties, required } = buildSchemaProperties(
120
+ schema.shape as Record<string, z.ZodType>
121
+ );
122
+ const result: Record<string, unknown> = {
123
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
124
+ properties,
125
+ type: 'object',
126
+ };
127
+ if (options?.title) {
128
+ result['title'] = options.title;
129
+ }
130
+ if (options?.description) {
131
+ result['description'] = options.description;
132
+ }
133
+ if (required.length > 0) {
134
+ result['required'] = required;
135
+ }
136
+ return result;
137
+ };
package/src/index.ts ADDED
@@ -0,0 +1,44 @@
1
+ export {
2
+ appConfig,
3
+ type AppConfig,
4
+ type AppConfigExplainOptions,
5
+ type AppConfigOptions,
6
+ type ConfigFormat,
7
+ type ResolveOptions,
8
+ } from './app-config.js';
9
+ export { collectConfigMeta } from './collect.js';
10
+ export { collectServiceConfigs, type ServiceConfigEntry } from './compose.js';
11
+ export { defineConfig, type DefineConfigOptions } from './define-config.js';
12
+ export { describeConfig, type FieldDescription } from './describe.js';
13
+ export {
14
+ checkConfig,
15
+ type CheckResult,
16
+ type ConfigDiagnostic,
17
+ } from './doctor.js';
18
+ export { env, secret, deprecated, type ConfigFieldMeta } from './extensions.js';
19
+ export {
20
+ explainConfig,
21
+ type ExplainConfigOptions,
22
+ type ProvenanceEntry,
23
+ } from './explain.js';
24
+ export {
25
+ generateEnvExample,
26
+ generateExample,
27
+ generateJsonSchema,
28
+ } from './generate/index.js';
29
+ export { configLayer } from './config-layer.js';
30
+ export { configService } from './config-service.js';
31
+ export {
32
+ clearConfigState,
33
+ type ConfigState,
34
+ getConfigState,
35
+ registerConfigState,
36
+ } from './registry.js';
37
+ export { deepMerge } from './merge.js';
38
+ export { configRef, isConfigRef, type ConfigRef } from './ref.js';
39
+ export { resolveConfig, type ResolveConfigOptions } from './resolve.js';
40
+ export { configCheck } from './trails/config-check.js';
41
+ export { configDescribe } from './trails/config-describe.js';
42
+ export { configExplain } from './trails/config-explain.js';
43
+ export { configInit } from './trails/config-init.js';
44
+ export { ensureWorkspace } from './workspace.js';