@ontrails/config 1.0.0-beta.14 → 1.0.0-beta.16

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 (163) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +20 -28
  3. package/package.json +10 -2
  4. package/src/app-config.ts +23 -12
  5. package/src/compose.ts +16 -22
  6. package/src/{config-provision.ts → config-resource.ts} +5 -5
  7. package/src/define-config.ts +18 -18
  8. package/src/{generate → derive}/env.ts +3 -3
  9. package/src/{generate → derive}/example.ts +1 -1
  10. package/src/{generate → derive}/helpers.ts +1 -1
  11. package/src/derive/index.ts +3 -0
  12. package/src/{generate → derive}/json-schema.ts +1 -1
  13. package/src/{describe.ts → derive-fields.ts} +1 -1
  14. package/src/{explain.ts → derive-provenance.ts} +6 -6
  15. package/src/extensions.ts +1 -1
  16. package/src/index.ts +12 -19
  17. package/src/registry.ts +2 -2
  18. package/src/resolve.ts +17 -15
  19. package/src/trails/config-check.ts +3 -3
  20. package/src/trails/config-describe.ts +5 -5
  21. package/src/trails/config-explain.ts +13 -13
  22. package/src/trails/config-init.ts +10 -10
  23. package/.turbo/turbo-build.log +0 -1
  24. package/.turbo/turbo-lint.log +0 -3
  25. package/.turbo/turbo-typecheck.log +0 -1
  26. package/dist/app-config.d.ts +0 -65
  27. package/dist/app-config.d.ts.map +0 -1
  28. package/dist/app-config.js +0 -172
  29. package/dist/app-config.js.map +0 -1
  30. package/dist/collect.d.ts +0 -11
  31. package/dist/collect.d.ts.map +0 -1
  32. package/dist/collect.js +0 -81
  33. package/dist/collect.js.map +0 -1
  34. package/dist/compose.d.ts +0 -30
  35. package/dist/compose.d.ts.map +0 -1
  36. package/dist/compose.js +0 -21
  37. package/dist/compose.js.map +0 -1
  38. package/dist/config-gate.d.ts +0 -11
  39. package/dist/config-gate.d.ts.map +0 -1
  40. package/dist/config-gate.js +0 -6
  41. package/dist/config-gate.js.map +0 -1
  42. package/dist/config-layer.d.ts +0 -11
  43. package/dist/config-layer.d.ts.map +0 -1
  44. package/dist/config-layer.js +0 -6
  45. package/dist/config-layer.js.map +0 -1
  46. package/dist/config-provision.d.ts +0 -3
  47. package/dist/config-provision.d.ts.map +0 -1
  48. package/dist/config-provision.js +0 -26
  49. package/dist/config-provision.js.map +0 -1
  50. package/dist/config-service.d.ts +0 -3
  51. package/dist/config-service.d.ts.map +0 -1
  52. package/dist/config-service.js +0 -26
  53. package/dist/config-service.js.map +0 -1
  54. package/dist/define-config.d.ts +0 -61
  55. package/dist/define-config.d.ts.map +0 -1
  56. package/dist/define-config.js +0 -90
  57. package/dist/define-config.js.map +0 -1
  58. package/dist/describe.d.ts +0 -25
  59. package/dist/describe.d.ts.map +0 -1
  60. package/dist/describe.js +0 -147
  61. package/dist/describe.js.map +0 -1
  62. package/dist/doctor.d.ts +0 -27
  63. package/dist/doctor.d.ts.map +0 -1
  64. package/dist/doctor.js +0 -131
  65. package/dist/doctor.js.map +0 -1
  66. package/dist/explain.d.ts +0 -30
  67. package/dist/explain.d.ts.map +0 -1
  68. package/dist/explain.js +0 -101
  69. package/dist/explain.js.map +0 -1
  70. package/dist/extensions.d.ts +0 -38
  71. package/dist/extensions.d.ts.map +0 -1
  72. package/dist/extensions.js +0 -35
  73. package/dist/extensions.js.map +0 -1
  74. package/dist/generate/env.d.ts +0 -15
  75. package/dist/generate/env.d.ts.map +0 -1
  76. package/dist/generate/env.js +0 -65
  77. package/dist/generate/env.js.map +0 -1
  78. package/dist/generate/example.d.ts +0 -16
  79. package/dist/generate/example.d.ts.map +0 -1
  80. package/dist/generate/example.js +0 -138
  81. package/dist/generate/example.js.map +0 -1
  82. package/dist/generate/helpers.d.ts +0 -35
  83. package/dist/generate/helpers.d.ts.map +0 -1
  84. package/dist/generate/helpers.js +0 -116
  85. package/dist/generate/helpers.js.map +0 -1
  86. package/dist/generate/index.d.ts +0 -4
  87. package/dist/generate/index.d.ts.map +0 -1
  88. package/dist/generate/index.js +0 -4
  89. package/dist/generate/index.js.map +0 -1
  90. package/dist/generate/json-schema.d.ts +0 -18
  91. package/dist/generate/json-schema.d.ts.map +0 -1
  92. package/dist/generate/json-schema.js +0 -97
  93. package/dist/generate/json-schema.js.map +0 -1
  94. package/dist/index.d.ts +0 -21
  95. package/dist/index.d.ts.map +0 -1
  96. package/dist/index.js +0 -21
  97. package/dist/index.js.map +0 -1
  98. package/dist/merge.d.ts +0 -16
  99. package/dist/merge.d.ts.map +0 -1
  100. package/dist/merge.js +0 -34
  101. package/dist/merge.js.map +0 -1
  102. package/dist/ref.d.ts +0 -29
  103. package/dist/ref.d.ts.map +0 -1
  104. package/dist/ref.js +0 -30
  105. package/dist/ref.js.map +0 -1
  106. package/dist/registry.d.ts +0 -24
  107. package/dist/registry.d.ts.map +0 -1
  108. package/dist/registry.js +0 -12
  109. package/dist/registry.js.map +0 -1
  110. package/dist/resolve.d.ts +0 -21
  111. package/dist/resolve.d.ts.map +0 -1
  112. package/dist/resolve.js +0 -177
  113. package/dist/resolve.js.map +0 -1
  114. package/dist/secret-heuristics.d.ts +0 -10
  115. package/dist/secret-heuristics.d.ts.map +0 -1
  116. package/dist/secret-heuristics.js +0 -11
  117. package/dist/secret-heuristics.js.map +0 -1
  118. package/dist/trails/config-check.d.ts +0 -11
  119. package/dist/trails/config-check.d.ts.map +0 -1
  120. package/dist/trails/config-check.js +0 -53
  121. package/dist/trails/config-check.js.map +0 -1
  122. package/dist/trails/config-describe.d.ts +0 -12
  123. package/dist/trails/config-describe.d.ts.map +0 -1
  124. package/dist/trails/config-describe.js +0 -41
  125. package/dist/trails/config-describe.js.map +0 -1
  126. package/dist/trails/config-explain.d.ts +0 -8
  127. package/dist/trails/config-explain.d.ts.map +0 -1
  128. package/dist/trails/config-explain.js +0 -74
  129. package/dist/trails/config-explain.js.map +0 -1
  130. package/dist/trails/config-init.d.ts +0 -9
  131. package/dist/trails/config-init.d.ts.map +0 -1
  132. package/dist/trails/config-init.js +0 -78
  133. package/dist/trails/config-init.js.map +0 -1
  134. package/dist/workspace.d.ts +0 -9
  135. package/dist/workspace.d.ts.map +0 -1
  136. package/dist/workspace.js +0 -49
  137. package/dist/workspace.js.map +0 -1
  138. package/dist/zod-utils.d.ts +0 -14
  139. package/dist/zod-utils.d.ts.map +0 -1
  140. package/dist/zod-utils.js +0 -41
  141. package/dist/zod-utils.js.map +0 -1
  142. package/src/__tests__/app-config.test.ts +0 -329
  143. package/src/__tests__/compose.test.ts +0 -59
  144. package/src/__tests__/config-check.test.ts +0 -171
  145. package/src/__tests__/config-describe.test.ts +0 -154
  146. package/src/__tests__/config-explain.test.ts +0 -167
  147. package/src/__tests__/config-gate.test.ts +0 -53
  148. package/src/__tests__/config-init.test.ts +0 -210
  149. package/src/__tests__/config-provision.test.ts +0 -87
  150. package/src/__tests__/define-config.test.ts +0 -263
  151. package/src/__tests__/describe.test.ts +0 -158
  152. package/src/__tests__/doctor.test.ts +0 -172
  153. package/src/__tests__/explain.test.ts +0 -139
  154. package/src/__tests__/extensions.test.ts +0 -134
  155. package/src/__tests__/generate.test.ts +0 -269
  156. package/src/__tests__/ref.test.ts +0 -35
  157. package/src/__tests__/resolve.test.ts +0 -246
  158. package/src/__tests__/workspace.test.ts +0 -64
  159. package/src/config-gate.ts +0 -15
  160. package/src/generate/index.ts +0 -3
  161. package/src/workspace.ts +0 -56
  162. package/tsconfig.json +0 -9
  163. package/tsconfig.tsbuildinfo +0 -1
@@ -1,139 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { z } from 'zod';
3
-
4
- import { env, secret } from '../extensions.js';
5
- import { explainConfig } from '../explain.js';
6
-
7
- const schema = z.object({
8
- debug: z.boolean().default(false),
9
- host: z.string().default('localhost'),
10
- port: z.number().default(3000),
11
- });
12
-
13
- describe('explainConfig', () => {
14
- describe('default source', () => {
15
- test('reports "default" when no other source provides value', () => {
16
- const entries = explainConfig({
17
- resolved: { debug: false, host: 'localhost', port: 3000 },
18
- schema,
19
- });
20
-
21
- const host = entries.find((e) => e.path === 'host');
22
- expect(host?.source).toBe('default');
23
- expect(host?.value).toBe('localhost');
24
- });
25
- });
26
-
27
- describe('base overrides default', () => {
28
- test('reports "base" when base provides value', () => {
29
- const entries = explainConfig({
30
- base: { host: 'base.example.com' },
31
- resolved: { debug: false, host: 'base.example.com', port: 3000 },
32
- schema,
33
- });
34
-
35
- const host = entries.find((e) => e.path === 'host');
36
- expect(host?.source).toBe('base');
37
- expect(host?.value).toBe('base.example.com');
38
- });
39
- });
40
-
41
- describe('loadout overrides base', () => {
42
- test('reports "loadout" when loadout provides winning value', () => {
43
- const entries = explainConfig({
44
- base: { host: 'base.example.com' },
45
- loadout: { host: 'loadout.example.com' },
46
- resolved: { debug: false, host: 'loadout.example.com', port: 3000 },
47
- schema,
48
- });
49
-
50
- const host = entries.find((e) => e.path === 'host');
51
- expect(host?.source).toBe('loadout');
52
- expect(host?.value).toBe('loadout.example.com');
53
- });
54
- });
55
-
56
- describe('local overrides loadout', () => {
57
- test('reports "local" when local provides winning value', () => {
58
- const entries = explainConfig({
59
- base: { host: 'base.example.com' },
60
- loadout: { host: 'loadout.example.com' },
61
- local: { host: 'local.example.com' },
62
- resolved: { debug: false, host: 'local.example.com', port: 3000 },
63
- schema,
64
- });
65
-
66
- const host = entries.find((e) => e.path === 'host');
67
- expect(host?.source).toBe('local');
68
- });
69
- });
70
-
71
- describe('env overrides everything', () => {
72
- test('reports "env" when env provides winning value', () => {
73
- const envSchema = z.object({
74
- host: env(z.string(), 'APP_HOST').default('localhost'),
75
- port: z.number().default(3000),
76
- });
77
-
78
- const entries = explainConfig({
79
- base: { host: 'base.example.com' },
80
- env: { APP_HOST: 'env.example.com' },
81
- resolved: { host: 'env.example.com', port: 3000 },
82
- schema: envSchema,
83
- });
84
-
85
- const host = entries.find((e) => e.path === 'host');
86
- expect(host?.source).toBe('env');
87
- expect(host?.value).toBe('env.example.com');
88
- });
89
-
90
- test('walks optional nested object schemas for env-backed entries', () => {
91
- const envSchema = z.object({
92
- db: z
93
- .object({
94
- host: env(z.string(), 'DB_HOST').default('localhost'),
95
- })
96
- .optional(),
97
- });
98
-
99
- const entries = explainConfig({
100
- env: { DB_HOST: 'env.example.com' },
101
- resolved: { db: { host: 'env.example.com' } },
102
- schema: envSchema,
103
- });
104
-
105
- const host = entries.find((entry) => entry.path === 'db.host');
106
- expect(host?.source).toBe('env');
107
- expect(host?.value).toBe('env.example.com');
108
- });
109
- });
110
-
111
- describe('secret redaction', () => {
112
- test('redacts secret fields', () => {
113
- const secretSchema = z.object({
114
- apiKey: secret(env(z.string(), 'API_KEY')),
115
- host: z.string().default('localhost'),
116
- });
117
-
118
- const entries = explainConfig({
119
- env: { API_KEY: 'super-secret-key' },
120
- resolved: { apiKey: 'super-secret-key', host: 'localhost' },
121
- schema: secretSchema,
122
- });
123
-
124
- const apiKey = entries.find((e) => e.path === 'apiKey');
125
- expect(apiKey?.redacted).toBe(true);
126
- expect(apiKey?.value).toBe('[REDACTED]');
127
- });
128
-
129
- test('does not redact non-secret fields', () => {
130
- const entries = explainConfig({
131
- resolved: { debug: false, host: 'localhost', port: 3000 },
132
- schema,
133
- });
134
-
135
- const host = entries.find((e) => e.path === 'host');
136
- expect(host?.redacted).toBe(false);
137
- });
138
- });
139
- });
@@ -1,134 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { z, globalRegistry } from 'zod';
3
-
4
- import { env, secret, deprecated } from '../extensions.js';
5
- import type { ConfigFieldMeta } from '../extensions.js';
6
- import { collectConfigMeta } from '../collect.js';
7
-
8
- describe('env()', () => {
9
- test('attaches env var name to schema metadata', () => {
10
- const schema = env(z.string(), 'DATABASE_URL');
11
- const meta = globalRegistry.get(schema) as ConfigFieldMeta | undefined;
12
-
13
- expect(meta).toBeDefined();
14
- expect(meta?.env).toBe('DATABASE_URL');
15
- });
16
- });
17
-
18
- describe('secret()', () => {
19
- test('attaches secret flag to schema metadata', () => {
20
- const schema = secret(z.string());
21
- const meta = globalRegistry.get(schema) as ConfigFieldMeta | undefined;
22
-
23
- expect(meta).toBeDefined();
24
- expect(meta?.secret).toBe(true);
25
- });
26
- });
27
-
28
- describe('deprecated()', () => {
29
- test('attaches deprecation message to schema metadata', () => {
30
- const schema = deprecated(z.string(), 'Use NEW_VAR instead');
31
- const raw = globalRegistry.get(schema) as
32
- | Record<string, unknown>
33
- | undefined;
34
-
35
- expect(raw).toBeDefined();
36
- // Zod 4 reserves `deprecated` as boolean, so the message is stored
37
- // under `deprecationMessage` and the boolean flag is set.
38
- expect(raw?.['deprecated']).toBe(true);
39
- expect(raw?.['deprecationMessage']).toBe('Use NEW_VAR instead');
40
- });
41
- });
42
-
43
- describe('wrapper ordering', () => {
44
- test('metadata survives through .default() when applied BEFORE the transform', () => {
45
- const schema = env(z.string(), 'HOST').default('localhost');
46
- // Metadata lives on the inner type, not the wrapper
47
- const innerMeta = globalRegistry.get(schema.def.innerType) as
48
- | ConfigFieldMeta
49
- | undefined;
50
-
51
- expect(innerMeta).toBeDefined();
52
- expect(innerMeta?.env).toBe('HOST');
53
- });
54
-
55
- test('metadata is NOT on the wrapper when applied AFTER .default()', () => {
56
- // Applying env() after .default() attaches metadata to the ZodDefault wrapper,
57
- // but collectConfigMeta walks .def.innerType — so the inner string has no metadata.
58
- const schema = env(z.string().default('localhost'), 'HOST');
59
- // The wrapper itself has the metadata
60
- const wrapperMeta = globalRegistry.get(schema) as
61
- | ConfigFieldMeta
62
- | undefined;
63
- expect(wrapperMeta?.env).toBe('HOST');
64
-
65
- // But the inner type does NOT
66
- const innerMeta = globalRegistry.get(schema.def.innerType) as
67
- | ConfigFieldMeta
68
- | undefined;
69
- expect(innerMeta?.env).toBeUndefined();
70
- });
71
- });
72
-
73
- describe('composition', () => {
74
- test('multiple wrappers compose: secret(env()) has both env and secret', () => {
75
- const schema = secret(env(z.string(), 'DB_URL'));
76
- const meta = globalRegistry.get(schema) as ConfigFieldMeta | undefined;
77
-
78
- expect(meta).toBeDefined();
79
- expect(meta?.env).toBe('DB_URL');
80
- expect(meta?.secret).toBe(true);
81
- });
82
- });
83
-
84
- describe('describe() preservation', () => {
85
- test('.describe() text is preserved alongside custom metadata', () => {
86
- const schema = env(z.string().describe('The database host'), 'DB_HOST');
87
- const meta = globalRegistry.get(schema) as
88
- | Record<string, unknown>
89
- | undefined;
90
-
91
- expect(meta).toBeDefined();
92
- expect(meta?.env).toBe('DB_HOST');
93
- expect(meta?.description).toBe('The database host');
94
- });
95
- });
96
-
97
- describe('collectConfigMeta()', () => {
98
- test('walks an object schema and returns all field metadata', () => {
99
- const schema = z.object({
100
- apiKey: secret(env(z.string(), 'API_KEY')),
101
- host: env(z.string(), 'HOST').default('localhost'),
102
- oldVar: deprecated(z.string(), 'Use newVar instead').optional(),
103
- port: env(z.number(), 'PORT'),
104
- });
105
-
106
- const meta = collectConfigMeta(schema);
107
-
108
- expect(meta.get('host')).toEqual({ env: 'HOST' });
109
- expect(meta.get('port')).toEqual({ env: 'PORT' });
110
- expect(meta.get('apiKey')).toEqual({ env: 'API_KEY', secret: true });
111
- expect(meta.get('oldVar')).toEqual({ deprecated: 'Use newVar instead' });
112
- });
113
-
114
- test('handles nested objects with dot-separated paths', () => {
115
- const schema = z.object({
116
- cache: z.object({
117
- ttl: env(z.number(), 'CACHE_TTL').default(3600),
118
- }),
119
- db: z.object({
120
- host: env(z.string(), 'DB_HOST'),
121
- password: secret(env(z.string(), 'DB_PASSWORD')),
122
- }),
123
- });
124
-
125
- const meta = collectConfigMeta(schema);
126
-
127
- expect(meta.get('db.host')).toEqual({ env: 'DB_HOST' });
128
- expect(meta.get('db.password')).toEqual({
129
- env: 'DB_PASSWORD',
130
- secret: true,
131
- });
132
- expect(meta.get('cache.ttl')).toEqual({ env: 'CACHE_TTL' });
133
- });
134
- });
@@ -1,269 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { z } from 'zod';
3
-
4
- import { deprecated, env, secret } from '../extensions.js';
5
- import {
6
- generateEnvExample,
7
- generateExample,
8
- generateJsonSchema,
9
- } from '../generate/index.js';
10
-
11
- /** Shared test schema used across generator tests. */
12
- const testSchema = z.object({
13
- host: env(z.string().describe('The server hostname'), 'HOST').default(
14
- 'localhost'
15
- ),
16
- port: env(z.number().describe('The server port'), 'PORT').default(3000),
17
- verbose: z.boolean().describe('Enable verbose logging').default(false),
18
- });
19
-
20
- /** Schema with deprecated and secret fields. */
21
- const annotatedSchema = z.object({
22
- apiKey: secret(env(z.string().describe('API authentication key'), 'API_KEY')),
23
- oldEndpoint: deprecated(
24
- env(z.string().describe('Legacy API endpoint'), 'OLD_ENDPOINT'),
25
- 'Use newEndpoint instead'
26
- ),
27
- });
28
-
29
- /** Schema with nested objects. */
30
- const nestedSchema = z.object({
31
- db: z.object({
32
- host: env(z.string().describe('Database host'), 'DB_HOST').default(
33
- 'localhost'
34
- ),
35
- port: env(z.number().describe('Database port'), 'DB_PORT').default(5432),
36
- }),
37
- server: z.object({
38
- name: z.string().describe('Server name').default('app'),
39
- }),
40
- });
41
-
42
- describe('generateExample()', () => {
43
- describe('TOML format', () => {
44
- test('produces valid TOML with comments for descriptions', () => {
45
- const result = generateExample(testSchema, 'toml');
46
-
47
- expect(result).toContain('# The server hostname');
48
- expect(result).toContain('host = "localhost"');
49
- expect(result).toContain('# The server port');
50
- expect(result).toContain('port = 3000');
51
- expect(result).toContain('# Enable verbose logging');
52
- expect(result).toContain('verbose = false');
53
- });
54
-
55
- test('annotates deprecated fields in TOML', () => {
56
- const result = generateExample(annotatedSchema, 'toml');
57
-
58
- expect(result).toContain('# DEPRECATED: Use newEndpoint instead');
59
- });
60
-
61
- test('handles nested objects as TOML sections', () => {
62
- const result = generateExample(nestedSchema, 'toml');
63
-
64
- expect(result).toContain('[db]');
65
- expect(result).toContain('[server]');
66
- expect(result).toContain('host = "localhost"');
67
- expect(result).toContain('port = 5432');
68
- });
69
- });
70
-
71
- describe('JSON format', () => {
72
- test('produces valid JSON without comments', () => {
73
- const result = generateExample(testSchema, 'json');
74
- const parsed = JSON.parse(result);
75
-
76
- expect(parsed).toEqual({
77
- host: 'localhost',
78
- port: 3000,
79
- verbose: false,
80
- });
81
- });
82
-
83
- test('handles nested objects', () => {
84
- const result = generateExample(nestedSchema, 'json');
85
- const parsed = JSON.parse(result);
86
-
87
- expect(parsed).toHaveProperty('db');
88
- expect(parsed).toHaveProperty('server');
89
- expect(parsed.db.host).toBe('localhost');
90
- });
91
- });
92
-
93
- describe('JSONC format', () => {
94
- test('produces JSON with // comments for descriptions', () => {
95
- const result = generateExample(testSchema, 'jsonc');
96
-
97
- expect(result).toContain('// The server hostname');
98
- expect(result).toContain('"host"');
99
- expect(result).toContain('"localhost"');
100
- });
101
-
102
- test('annotates deprecated fields in JSONC', () => {
103
- const result = generateExample(annotatedSchema, 'jsonc');
104
-
105
- expect(result).toContain('// DEPRECATED: Use newEndpoint instead');
106
- });
107
-
108
- test('handles nested objects', () => {
109
- const result = generateExample(nestedSchema, 'jsonc');
110
-
111
- expect(result).toContain('"db"');
112
- expect(result).toContain('"host"');
113
- expect(result).toContain('"localhost"');
114
- expect(result).not.toContain('"db": ""');
115
- });
116
- });
117
-
118
- describe('YAML format', () => {
119
- test('produces valid YAML with comments for descriptions', () => {
120
- const result = generateExample(testSchema, 'yaml');
121
-
122
- expect(result).toContain('# The server hostname');
123
- expect(result).toContain('host: "localhost"');
124
- expect(result).toContain('# The server port');
125
- expect(result).toContain('port: 3000');
126
- expect(result).toContain('# Enable verbose logging');
127
- expect(result).toContain('verbose: false');
128
- });
129
-
130
- test('annotates deprecated fields in YAML', () => {
131
- const result = generateExample(annotatedSchema, 'yaml');
132
-
133
- expect(result).toContain('# DEPRECATED: Use newEndpoint instead');
134
- });
135
-
136
- test('handles nested objects', () => {
137
- const result = generateExample(nestedSchema, 'yaml');
138
-
139
- expect(result).toContain('db:');
140
- expect(result).toContain(' host: "localhost"');
141
- expect(result).toContain('server:');
142
- });
143
- });
144
- });
145
-
146
- describe('generateJsonSchema()', () => {
147
- test('produces valid JSON Schema with $schema, type, and properties', () => {
148
- const result = generateJsonSchema(testSchema);
149
-
150
- expect(result.$schema).toBe('https://json-schema.org/draft/2020-12/schema');
151
- expect(result.type).toBe('object');
152
- expect(result.properties).toBeDefined();
153
- });
154
-
155
- test('includes title and description from options', () => {
156
- const result = generateJsonSchema(testSchema, {
157
- description: 'Server configuration',
158
- title: 'ServerConfig',
159
- });
160
-
161
- expect(result.title).toBe('ServerConfig');
162
- expect(result.description).toBe('Server configuration');
163
- });
164
-
165
- test('includes descriptions from .describe()', () => {
166
- const result = generateJsonSchema(testSchema);
167
- const props = result.properties as Record<string, Record<string, unknown>>;
168
-
169
- expect(props['host']?.description).toBe('The server hostname');
170
- expect(props['port']?.description).toBe('The server port');
171
- });
172
-
173
- test('includes defaults', () => {
174
- const result = generateJsonSchema(testSchema);
175
- const props = result.properties as Record<string, Record<string, unknown>>;
176
-
177
- expect(props['host']?.default).toBe('localhost');
178
- expect(props['port']?.default).toBe(3000);
179
- expect(props['verbose']?.default).toBe(false);
180
- });
181
-
182
- test('maps string, number, boolean, and enum types correctly', () => {
183
- const enumSchema = z.object({
184
- color: z.enum(['red', 'green', 'blue']).describe('The color'),
185
- count: z.number().describe('A count'),
186
- enabled: z.boolean().describe('Toggle'),
187
- name: z.string().describe('A name'),
188
- });
189
-
190
- const result = generateJsonSchema(enumSchema);
191
- const props = result.properties as Record<string, Record<string, unknown>>;
192
-
193
- expect(props['name']?.type).toBe('string');
194
- expect(props['count']?.type).toBe('number');
195
- expect(props['enabled']?.type).toBe('boolean');
196
- expect(props['color']?.enum).toEqual(['red', 'green', 'blue']);
197
- });
198
-
199
- test('marks deprecated fields', () => {
200
- const result = generateJsonSchema(annotatedSchema);
201
- const props = result.properties as Record<string, Record<string, unknown>>;
202
-
203
- expect(props['oldEndpoint']?.deprecated).toBe(true);
204
- });
205
-
206
- test('lists required fields (those without defaults or optional)', () => {
207
- const result = generateJsonSchema(annotatedSchema);
208
-
209
- expect(result.required).toContain('apiKey');
210
- expect(result.required).toContain('oldEndpoint');
211
- });
212
-
213
- test('recurses into nested object fields', () => {
214
- const result = generateJsonSchema(nestedSchema);
215
- const props = result.properties as Record<string, Record<string, unknown>>;
216
-
217
- expect(props['db']?.type).toBe('object');
218
- const dbProps = props['db']?.properties as Record<
219
- string,
220
- Record<string, unknown>
221
- >;
222
- expect(dbProps['host']?.type).toBe('string');
223
- expect(dbProps['host']?.description).toBe('Database host');
224
- expect(dbProps['port']?.type).toBe('number');
225
-
226
- expect(props['server']?.type).toBe('object');
227
- const serverProps = props['server']?.properties as Record<
228
- string,
229
- Record<string, unknown>
230
- >;
231
- expect(serverProps['name']?.type).toBe('string');
232
- });
233
- });
234
-
235
- describe('generateEnvExample()', () => {
236
- test('lists env vars with type info', () => {
237
- const result = generateEnvExample(testSchema);
238
-
239
- expect(result).toContain('HOST=');
240
- expect(result).toContain('PORT=');
241
- expect(result).toContain('string');
242
- expect(result).toContain('number');
243
- });
244
-
245
- test('annotates secrets', () => {
246
- const result = generateEnvExample(annotatedSchema);
247
-
248
- expect(result).toContain('API_KEY=');
249
- expect(result).toContain('secret');
250
- });
251
-
252
- test('shows defaults as comments', () => {
253
- const result = generateEnvExample(testSchema);
254
-
255
- expect(result).toContain('default: "localhost"');
256
- expect(result).toContain('default: 3000');
257
- });
258
-
259
- test('returns empty string when no env vars are present', () => {
260
- const noEnvSchema = z.object({
261
- name: z.string(),
262
- verbose: z.boolean().default(false),
263
- });
264
-
265
- const result = generateEnvExample(noEnvSchema);
266
-
267
- expect(result).toBe('');
268
- });
269
- });
@@ -1,35 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import { configRef, isConfigRef } from '../ref.js';
4
-
5
- describe('configRef', () => {
6
- test('creates a marker object with __configRef flag', () => {
7
- const ref = configRef('db.host');
8
- expect(ref.__configRef).toBe(true);
9
- expect(ref.path).toBe('db.host');
10
- });
11
-
12
- test('creates distinct refs for different paths', () => {
13
- const ref1 = configRef('db.host');
14
- const ref2 = configRef('db.port');
15
- expect(ref1.path).not.toBe(ref2.path);
16
- });
17
- });
18
-
19
- describe('isConfigRef', () => {
20
- test('returns true for configRef markers', () => {
21
- const ref = configRef('db.host');
22
- expect(isConfigRef(ref)).toBe(true);
23
- });
24
-
25
- test('returns false for plain objects', () => {
26
- expect(isConfigRef({ path: 'db.host' })).toBe(false);
27
- });
28
-
29
- test('returns false for non-objects', () => {
30
- expect(isConfigRef('db.host')).toBe(false);
31
- expect(isConfigRef(42)).toBe(false);
32
- expect(isConfigRef(null)).toBe(false);
33
- expect(isConfigRef()).toBe(false);
34
- });
35
- });