@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.
- package/CHANGELOG.md +41 -0
- package/README.md +20 -28
- package/package.json +10 -2
- package/src/app-config.ts +23 -12
- package/src/compose.ts +16 -22
- package/src/{config-provision.ts → config-resource.ts} +5 -5
- package/src/define-config.ts +18 -18
- package/src/{generate → derive}/env.ts +3 -3
- package/src/{generate → derive}/example.ts +1 -1
- package/src/{generate → derive}/helpers.ts +1 -1
- package/src/derive/index.ts +3 -0
- package/src/{generate → derive}/json-schema.ts +1 -1
- package/src/{describe.ts → derive-fields.ts} +1 -1
- package/src/{explain.ts → derive-provenance.ts} +6 -6
- package/src/extensions.ts +1 -1
- package/src/index.ts +12 -19
- package/src/registry.ts +2 -2
- package/src/resolve.ts +17 -15
- package/src/trails/config-check.ts +3 -3
- package/src/trails/config-describe.ts +5 -5
- package/src/trails/config-explain.ts +13 -13
- package/src/trails/config-init.ts +10 -10
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/app-config.d.ts +0 -65
- package/dist/app-config.d.ts.map +0 -1
- package/dist/app-config.js +0 -172
- package/dist/app-config.js.map +0 -1
- package/dist/collect.d.ts +0 -11
- package/dist/collect.d.ts.map +0 -1
- package/dist/collect.js +0 -81
- package/dist/collect.js.map +0 -1
- package/dist/compose.d.ts +0 -30
- package/dist/compose.d.ts.map +0 -1
- package/dist/compose.js +0 -21
- package/dist/compose.js.map +0 -1
- package/dist/config-gate.d.ts +0 -11
- package/dist/config-gate.d.ts.map +0 -1
- package/dist/config-gate.js +0 -6
- package/dist/config-gate.js.map +0 -1
- package/dist/config-layer.d.ts +0 -11
- package/dist/config-layer.d.ts.map +0 -1
- package/dist/config-layer.js +0 -6
- package/dist/config-layer.js.map +0 -1
- package/dist/config-provision.d.ts +0 -3
- package/dist/config-provision.d.ts.map +0 -1
- package/dist/config-provision.js +0 -26
- package/dist/config-provision.js.map +0 -1
- package/dist/config-service.d.ts +0 -3
- package/dist/config-service.d.ts.map +0 -1
- package/dist/config-service.js +0 -26
- package/dist/config-service.js.map +0 -1
- package/dist/define-config.d.ts +0 -61
- package/dist/define-config.d.ts.map +0 -1
- package/dist/define-config.js +0 -90
- package/dist/define-config.js.map +0 -1
- package/dist/describe.d.ts +0 -25
- package/dist/describe.d.ts.map +0 -1
- package/dist/describe.js +0 -147
- package/dist/describe.js.map +0 -1
- package/dist/doctor.d.ts +0 -27
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js +0 -131
- package/dist/doctor.js.map +0 -1
- package/dist/explain.d.ts +0 -30
- package/dist/explain.d.ts.map +0 -1
- package/dist/explain.js +0 -101
- package/dist/explain.js.map +0 -1
- package/dist/extensions.d.ts +0 -38
- package/dist/extensions.d.ts.map +0 -1
- package/dist/extensions.js +0 -35
- package/dist/extensions.js.map +0 -1
- package/dist/generate/env.d.ts +0 -15
- package/dist/generate/env.d.ts.map +0 -1
- package/dist/generate/env.js +0 -65
- package/dist/generate/env.js.map +0 -1
- package/dist/generate/example.d.ts +0 -16
- package/dist/generate/example.d.ts.map +0 -1
- package/dist/generate/example.js +0 -138
- package/dist/generate/example.js.map +0 -1
- package/dist/generate/helpers.d.ts +0 -35
- package/dist/generate/helpers.d.ts.map +0 -1
- package/dist/generate/helpers.js +0 -116
- package/dist/generate/helpers.js.map +0 -1
- package/dist/generate/index.d.ts +0 -4
- package/dist/generate/index.d.ts.map +0 -1
- package/dist/generate/index.js +0 -4
- package/dist/generate/index.js.map +0 -1
- package/dist/generate/json-schema.d.ts +0 -18
- package/dist/generate/json-schema.d.ts.map +0 -1
- package/dist/generate/json-schema.js +0 -97
- package/dist/generate/json-schema.js.map +0 -1
- package/dist/index.d.ts +0 -21
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -21
- package/dist/index.js.map +0 -1
- package/dist/merge.d.ts +0 -16
- package/dist/merge.d.ts.map +0 -1
- package/dist/merge.js +0 -34
- package/dist/merge.js.map +0 -1
- package/dist/ref.d.ts +0 -29
- package/dist/ref.d.ts.map +0 -1
- package/dist/ref.js +0 -30
- package/dist/ref.js.map +0 -1
- package/dist/registry.d.ts +0 -24
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -12
- package/dist/registry.js.map +0 -1
- package/dist/resolve.d.ts +0 -21
- package/dist/resolve.d.ts.map +0 -1
- package/dist/resolve.js +0 -177
- package/dist/resolve.js.map +0 -1
- package/dist/secret-heuristics.d.ts +0 -10
- package/dist/secret-heuristics.d.ts.map +0 -1
- package/dist/secret-heuristics.js +0 -11
- package/dist/secret-heuristics.js.map +0 -1
- package/dist/trails/config-check.d.ts +0 -11
- package/dist/trails/config-check.d.ts.map +0 -1
- package/dist/trails/config-check.js +0 -53
- package/dist/trails/config-check.js.map +0 -1
- package/dist/trails/config-describe.d.ts +0 -12
- package/dist/trails/config-describe.d.ts.map +0 -1
- package/dist/trails/config-describe.js +0 -41
- package/dist/trails/config-describe.js.map +0 -1
- package/dist/trails/config-explain.d.ts +0 -8
- package/dist/trails/config-explain.d.ts.map +0 -1
- package/dist/trails/config-explain.js +0 -74
- package/dist/trails/config-explain.js.map +0 -1
- package/dist/trails/config-init.d.ts +0 -9
- package/dist/trails/config-init.d.ts.map +0 -1
- package/dist/trails/config-init.js +0 -78
- package/dist/trails/config-init.js.map +0 -1
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -49
- package/dist/workspace.js.map +0 -1
- package/dist/zod-utils.d.ts +0 -14
- package/dist/zod-utils.d.ts.map +0 -1
- package/dist/zod-utils.js +0 -41
- package/dist/zod-utils.js.map +0 -1
- package/src/__tests__/app-config.test.ts +0 -329
- package/src/__tests__/compose.test.ts +0 -59
- package/src/__tests__/config-check.test.ts +0 -171
- package/src/__tests__/config-describe.test.ts +0 -154
- package/src/__tests__/config-explain.test.ts +0 -167
- package/src/__tests__/config-gate.test.ts +0 -53
- package/src/__tests__/config-init.test.ts +0 -210
- package/src/__tests__/config-provision.test.ts +0 -87
- package/src/__tests__/define-config.test.ts +0 -263
- package/src/__tests__/describe.test.ts +0 -158
- package/src/__tests__/doctor.test.ts +0 -172
- package/src/__tests__/explain.test.ts +0 -139
- package/src/__tests__/extensions.test.ts +0 -134
- package/src/__tests__/generate.test.ts +0 -269
- package/src/__tests__/ref.test.ts +0 -35
- package/src/__tests__/resolve.test.ts +0 -246
- package/src/__tests__/workspace.test.ts +0 -64
- package/src/config-gate.ts +0 -15
- package/src/generate/index.ts +0 -3
- package/src/workspace.ts +0 -56
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
|
|
7
|
-
import { defineConfig } from '../define-config.js';
|
|
8
|
-
|
|
9
|
-
const schema = z.object({
|
|
10
|
-
debug: z.boolean().default(false),
|
|
11
|
-
host: z.string().default('localhost'),
|
|
12
|
-
port: z.number().default(3000),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
type EnvKey = 'NODE_ENV' | 'TRAILS_ENV';
|
|
16
|
-
|
|
17
|
-
interface EnvSnapshot {
|
|
18
|
-
readonly NODE_ENV: string | undefined;
|
|
19
|
-
readonly TRAILS_ENV: string | undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const readEnvSnapshot = (): EnvSnapshot => ({
|
|
23
|
-
NODE_ENV: process.env.NODE_ENV,
|
|
24
|
-
TRAILS_ENV: process.env.TRAILS_ENV,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const setEnvVar = (key: EnvKey, value: string | undefined): void => {
|
|
28
|
-
if (value === undefined) {
|
|
29
|
-
if (key === 'NODE_ENV') {
|
|
30
|
-
delete process.env.NODE_ENV;
|
|
31
|
-
} else {
|
|
32
|
-
delete process.env.TRAILS_ENV;
|
|
33
|
-
}
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (key === 'NODE_ENV') {
|
|
37
|
-
process.env.NODE_ENV = value;
|
|
38
|
-
} else {
|
|
39
|
-
process.env.TRAILS_ENV = value;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const restoreEnv = (snapshot: EnvSnapshot): void => {
|
|
44
|
-
setEnvVar('NODE_ENV', snapshot.NODE_ENV);
|
|
45
|
-
setEnvVar('TRAILS_ENV', snapshot.TRAILS_ENV);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
describe('defineConfig', () => {
|
|
49
|
-
test('returns an object with schema, base, and loadouts', () => {
|
|
50
|
-
const base = { host: 'example.com' };
|
|
51
|
-
const loadouts = { production: { port: 443 } };
|
|
52
|
-
|
|
53
|
-
const config = defineConfig({ base, loadouts, schema });
|
|
54
|
-
|
|
55
|
-
expect(config.schema).toBe(schema);
|
|
56
|
-
expect(config.base).toBe(base);
|
|
57
|
-
expect(config.loadouts).toBe(loadouts);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('resolve() uses TRAILS_ENV to select loadout', async () => {
|
|
61
|
-
const config = defineConfig({
|
|
62
|
-
base: { host: 'example.com' },
|
|
63
|
-
loadouts: {
|
|
64
|
-
production: { host: 'prod.example.com', port: 443 },
|
|
65
|
-
test: { host: 'test.example.com', port: 9999 },
|
|
66
|
-
},
|
|
67
|
-
schema,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const result = await config.resolve({
|
|
71
|
-
env: { TRAILS_ENV: 'production' },
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
expect(result.isOk()).toBe(true);
|
|
75
|
-
expect(result.unwrap().host).toBe('prod.example.com');
|
|
76
|
-
expect(result.unwrap().port).toBe(443);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('resolve() with explicit loadout option overrides TRAILS_ENV', async () => {
|
|
80
|
-
const config = defineConfig({
|
|
81
|
-
base: { host: 'example.com' },
|
|
82
|
-
loadouts: {
|
|
83
|
-
production: { host: 'prod.example.com' },
|
|
84
|
-
test: { host: 'test.example.com' },
|
|
85
|
-
},
|
|
86
|
-
schema,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const result = await config.resolve({
|
|
90
|
-
env: { TRAILS_ENV: 'production' },
|
|
91
|
-
loadout: 'test',
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
expect(result.isOk()).toBe(true);
|
|
95
|
-
expect(result.unwrap().host).toBe('test.example.com');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('envFromNodeEnv maps NODE_ENV to TRAILS_ENV when unset', async () => {
|
|
99
|
-
const config = defineConfig({
|
|
100
|
-
base: { host: 'example.com' },
|
|
101
|
-
envFromNodeEnv: true,
|
|
102
|
-
loadouts: {
|
|
103
|
-
production: { host: 'prod.example.com', port: 443 },
|
|
104
|
-
},
|
|
105
|
-
schema,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const result = await config.resolve({
|
|
109
|
-
env: { NODE_ENV: 'production' },
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
expect(result.isOk()).toBe(true);
|
|
113
|
-
expect(result.unwrap().host).toBe('prod.example.com');
|
|
114
|
-
expect(result.unwrap().port).toBe(443);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('envFromNodeEnv does not mutate process.env', async () => {
|
|
118
|
-
const snapshot = readEnvSnapshot();
|
|
119
|
-
setEnvVar('TRAILS_ENV', undefined);
|
|
120
|
-
setEnvVar('NODE_ENV', 'production');
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const config = defineConfig({
|
|
124
|
-
base: { host: 'example.com' },
|
|
125
|
-
envFromNodeEnv: true,
|
|
126
|
-
loadouts: {
|
|
127
|
-
production: { host: 'prod.example.com', port: 443 },
|
|
128
|
-
},
|
|
129
|
-
schema,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const result = await config.resolve();
|
|
133
|
-
|
|
134
|
-
expect(result.isOk()).toBe(true);
|
|
135
|
-
expect(result.unwrap().host).toBe('prod.example.com');
|
|
136
|
-
expect(process.env.TRAILS_ENV).toBeUndefined();
|
|
137
|
-
} finally {
|
|
138
|
-
restoreEnv(snapshot);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('envFromNodeEnv does not override explicit TRAILS_ENV', async () => {
|
|
143
|
-
const config = defineConfig({
|
|
144
|
-
base: { host: 'example.com' },
|
|
145
|
-
envFromNodeEnv: true,
|
|
146
|
-
loadouts: {
|
|
147
|
-
production: { host: 'prod.example.com' },
|
|
148
|
-
test: { host: 'test.example.com' },
|
|
149
|
-
},
|
|
150
|
-
schema,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const result = await config.resolve({
|
|
154
|
-
env: { NODE_ENV: 'production', TRAILS_ENV: 'test' },
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
expect(result.isOk()).toBe(true);
|
|
158
|
-
expect(result.unwrap().host).toBe('test.example.com');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('test loadout works when TRAILS_ENV=test', async () => {
|
|
162
|
-
const config = defineConfig({
|
|
163
|
-
base: { port: 8080 },
|
|
164
|
-
loadouts: {
|
|
165
|
-
test: { debug: true, port: 0 },
|
|
166
|
-
},
|
|
167
|
-
schema,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const result = await config.resolve({
|
|
171
|
-
env: { TRAILS_ENV: 'test' },
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
expect(result.isOk()).toBe(true);
|
|
175
|
-
const value = result.unwrap();
|
|
176
|
-
expect(value.debug).toBe(true);
|
|
177
|
-
expect(value.port).toBe(0);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
describe('local overrides', () => {
|
|
181
|
-
let tempDir: string;
|
|
182
|
-
|
|
183
|
-
beforeEach(async () => {
|
|
184
|
-
tempDir = await mkdtemp(join(tmpdir(), 'trails-define-config-'));
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
afterEach(async () => {
|
|
188
|
-
await rm(tempDir, { force: true, recursive: true });
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('applies local overrides from .trails/config/local.ts', async () => {
|
|
192
|
-
const configDir = join(tempDir, '.trails', 'config');
|
|
193
|
-
await mkdir(configDir, { recursive: true });
|
|
194
|
-
await Bun.write(
|
|
195
|
-
join(configDir, 'local.ts'),
|
|
196
|
-
'export default { port: 4444 };'
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const config = defineConfig({
|
|
200
|
-
base: { host: 'example.com' },
|
|
201
|
-
schema,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const result = await config.resolve({
|
|
205
|
-
cwd: tempDir,
|
|
206
|
-
env: {},
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
expect(result.isOk()).toBe(true);
|
|
210
|
-
expect(result.unwrap().port).toBe(4444);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('local overrides applied between loadout and env', async () => {
|
|
214
|
-
const configDir = join(tempDir, '.trails', 'config');
|
|
215
|
-
await mkdir(configDir, { recursive: true });
|
|
216
|
-
await Bun.write(
|
|
217
|
-
join(configDir, 'local.js'),
|
|
218
|
-
'export default { port: 5555 };'
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const config = defineConfig({
|
|
222
|
-
base: { port: 8080 },
|
|
223
|
-
loadouts: {
|
|
224
|
-
dev: { port: 9090 },
|
|
225
|
-
},
|
|
226
|
-
schema,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// Local overrides should win over loadout (9090)
|
|
230
|
-
const result = await config.resolve({
|
|
231
|
-
cwd: tempDir,
|
|
232
|
-
env: { TRAILS_ENV: 'dev' },
|
|
233
|
-
loadout: 'dev',
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
expect(result.isOk()).toBe(true);
|
|
237
|
-
expect(result.unwrap().port).toBe(5555);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test('skips local overrides when TRAILS_ENV=test', async () => {
|
|
241
|
-
const configDir = join(tempDir, '.trails', 'config');
|
|
242
|
-
await mkdir(configDir, { recursive: true });
|
|
243
|
-
await Bun.write(
|
|
244
|
-
join(configDir, 'local.ts'),
|
|
245
|
-
'export default { port: 4444 };'
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const config = defineConfig({
|
|
249
|
-
base: { port: 8080 },
|
|
250
|
-
schema,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const result = await config.resolve({
|
|
254
|
-
cwd: tempDir,
|
|
255
|
-
env: { TRAILS_ENV: 'test' },
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
expect(result.isOk()).toBe(true);
|
|
259
|
-
// Should NOT apply local overrides, so port stays at base 8080
|
|
260
|
-
expect(result.unwrap().port).toBe(8080);
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
});
|
|
@@ -1,158 +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 { describeConfig } from '../describe.js';
|
|
6
|
-
|
|
7
|
-
describe('describeConfig', () => {
|
|
8
|
-
describe('basic field descriptions', () => {
|
|
9
|
-
test('returns path, type, and required for each field', () => {
|
|
10
|
-
const schema = z.object({
|
|
11
|
-
debug: z.boolean(),
|
|
12
|
-
host: z.string(),
|
|
13
|
-
port: z.number(),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const fields = describeConfig(schema);
|
|
17
|
-
|
|
18
|
-
expect(fields).toHaveLength(3);
|
|
19
|
-
const host = fields.find((f) => f.path === 'host');
|
|
20
|
-
expect(host?.type).toBe('string');
|
|
21
|
-
expect(host?.required).toBe(true);
|
|
22
|
-
|
|
23
|
-
const port = fields.find((f) => f.path === 'port');
|
|
24
|
-
expect(port?.type).toBe('number');
|
|
25
|
-
expect(port?.required).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('Zod describe() metadata', () => {
|
|
30
|
-
test('includes description from .describe()', () => {
|
|
31
|
-
const schema = z.object({
|
|
32
|
-
host: z.string().describe('The server hostname'),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const fields = describeConfig(schema);
|
|
36
|
-
const host = fields.find((f) => f.path === 'host');
|
|
37
|
-
expect(host?.description).toBe('The server hostname');
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('config metadata', () => {
|
|
42
|
-
test('includes env, secret, deprecated from metadata', () => {
|
|
43
|
-
const schema = z.object({
|
|
44
|
-
apiKey: secret(env(z.string(), 'API_KEY')),
|
|
45
|
-
legacyMode: deprecated(z.boolean(), 'Use "mode" instead').default(
|
|
46
|
-
false
|
|
47
|
-
),
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const fields = describeConfig(schema);
|
|
51
|
-
|
|
52
|
-
const apiKey = fields.find((f) => f.path === 'apiKey');
|
|
53
|
-
expect(apiKey?.env).toBe('API_KEY');
|
|
54
|
-
expect(apiKey?.secret).toBe(true);
|
|
55
|
-
|
|
56
|
-
const legacy = fields.find((f) => f.path === 'legacyMode');
|
|
57
|
-
expect(legacy?.deprecated).toBe('Use "mode" instead');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('defaults', () => {
|
|
62
|
-
test('detects default values', () => {
|
|
63
|
-
const schema = z.object({
|
|
64
|
-
debug: z.boolean().default(false),
|
|
65
|
-
port: z.number().default(3000),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const fields = describeConfig(schema);
|
|
69
|
-
|
|
70
|
-
const port = fields.find((f) => f.path === 'port');
|
|
71
|
-
expect(port?.default).toBe(3000);
|
|
72
|
-
expect(port?.required).toBe(false);
|
|
73
|
-
|
|
74
|
-
const debug = fields.find((f) => f.path === 'debug');
|
|
75
|
-
expect(debug?.default).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('nested objects', () => {
|
|
80
|
-
test('handles nested object schemas with dot paths', () => {
|
|
81
|
-
const schema = z.object({
|
|
82
|
-
db: z.object({
|
|
83
|
-
host: z.string(),
|
|
84
|
-
port: z.number().default(5432),
|
|
85
|
-
}),
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const fields = describeConfig(schema);
|
|
89
|
-
|
|
90
|
-
const dbHost = fields.find((f) => f.path === 'db.host');
|
|
91
|
-
expect(dbHost?.type).toBe('string');
|
|
92
|
-
expect(dbHost?.required).toBe(true);
|
|
93
|
-
|
|
94
|
-
const dbPort = fields.find((f) => f.path === 'db.port');
|
|
95
|
-
expect(dbPort?.type).toBe('number');
|
|
96
|
-
expect(dbPort?.default).toBe(5432);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('unwraps optional nested object schemas before walking', () => {
|
|
100
|
-
const schema = z.object({
|
|
101
|
-
db: z
|
|
102
|
-
.object({
|
|
103
|
-
host: z.string(),
|
|
104
|
-
port: z.number().default(5432),
|
|
105
|
-
})
|
|
106
|
-
.optional(),
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const fields = describeConfig(schema);
|
|
110
|
-
|
|
111
|
-
expect(fields).toEqual([
|
|
112
|
-
expect.objectContaining({ path: 'db.host', required: true }),
|
|
113
|
-
expect.objectContaining({ default: 5432, path: 'db.port' }),
|
|
114
|
-
]);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('constraints', () => {
|
|
119
|
-
test('detects enum values', () => {
|
|
120
|
-
const schema = z.object({
|
|
121
|
-
env: z.enum(['development', 'production', 'test']),
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const fields = describeConfig(schema);
|
|
125
|
-
const envField = fields.find((f) => f.path === 'env');
|
|
126
|
-
expect(envField?.type).toBe('enum');
|
|
127
|
-
expect(envField?.constraints?.values).toEqual([
|
|
128
|
-
'development',
|
|
129
|
-
'production',
|
|
130
|
-
'test',
|
|
131
|
-
]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test('detects number min/max constraints', () => {
|
|
135
|
-
const schema = z.object({
|
|
136
|
-
port: z.number().min(1).max(65_535),
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const fields = describeConfig(schema);
|
|
140
|
-
const port = fields.find((f) => f.path === 'port');
|
|
141
|
-
expect(port?.constraints?.min).toBe(1);
|
|
142
|
-
expect(port?.constraints?.max).toBe(65_535);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe('optional fields', () => {
|
|
147
|
-
test('marks optional fields as not required', () => {
|
|
148
|
-
const schema = z.object({
|
|
149
|
-
host: z.string(),
|
|
150
|
-
nickname: z.string().optional(),
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const fields = describeConfig(schema);
|
|
154
|
-
const nickname = fields.find((f) => f.path === 'nickname');
|
|
155
|
-
expect(nickname?.required).toBe(false);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
});
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
|
|
4
|
-
import { deprecated, env } from '../extensions.js';
|
|
5
|
-
import { checkConfig } from '../doctor.js';
|
|
6
|
-
|
|
7
|
-
const schema = z.object({
|
|
8
|
-
debug: z.boolean().default(false),
|
|
9
|
-
host: z.string(),
|
|
10
|
-
port: z.number().default(3000),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe('checkConfig', () => {
|
|
14
|
-
describe('valid fields', () => {
|
|
15
|
-
test('reports status "valid" for present and valid fields', () => {
|
|
16
|
-
const result = checkConfig(schema, { host: 'localhost', port: 8080 });
|
|
17
|
-
|
|
18
|
-
const hostDiag = result.diagnostics.find((d) => d.path === 'host');
|
|
19
|
-
expect(hostDiag?.status).toBe('valid');
|
|
20
|
-
expect(hostDiag?.value).toBe('localhost');
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('missing required fields', () => {
|
|
25
|
-
test('reports status "missing" for absent required fields', () => {
|
|
26
|
-
const result = checkConfig(schema, { port: 8080 });
|
|
27
|
-
|
|
28
|
-
const hostDiag = result.diagnostics.find((d) => d.path === 'host');
|
|
29
|
-
expect(hostDiag?.status).toBe('missing');
|
|
30
|
-
expect(hostDiag?.message).toContain('host');
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('default fields', () => {
|
|
35
|
-
test('reports status "default" when field uses schema default', () => {
|
|
36
|
-
const result = checkConfig(schema, { host: 'localhost' });
|
|
37
|
-
|
|
38
|
-
const portDiag = result.diagnostics.find((d) => d.path === 'port');
|
|
39
|
-
expect(portDiag?.status).toBe('default');
|
|
40
|
-
expect(portDiag?.value).toBe(3000);
|
|
41
|
-
|
|
42
|
-
const debugDiag = result.diagnostics.find((d) => d.path === 'debug');
|
|
43
|
-
expect(debugDiag?.status).toBe('default');
|
|
44
|
-
expect(debugDiag?.value).toBe(false);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('invalid fields', () => {
|
|
49
|
-
test('reports status "invalid" with message for wrong-type values', () => {
|
|
50
|
-
const result = checkConfig(schema, {
|
|
51
|
-
host: 'localhost',
|
|
52
|
-
port: 'not-a-number',
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const portDiag = result.diagnostics.find((d) => d.path === 'port');
|
|
56
|
-
expect(portDiag?.status).toBe('invalid');
|
|
57
|
-
expect(portDiag?.message).toBeDefined();
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('deprecated fields', () => {
|
|
62
|
-
test('reports status "deprecated" with migration message', () => {
|
|
63
|
-
const deprecatedSchema = z.object({
|
|
64
|
-
host: z.string(),
|
|
65
|
-
legacyMode: deprecated(z.boolean(), 'Use "mode" instead').default(
|
|
66
|
-
false
|
|
67
|
-
),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const result = checkConfig(deprecatedSchema, {
|
|
71
|
-
host: 'localhost',
|
|
72
|
-
legacyMode: true,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const legacyDiag = result.diagnostics.find(
|
|
76
|
-
(d) => d.path === 'legacyMode'
|
|
77
|
-
);
|
|
78
|
-
expect(legacyDiag?.status).toBe('deprecated');
|
|
79
|
-
expect(legacyDiag?.message).toContain('Use "mode" instead');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('valid flag', () => {
|
|
84
|
-
test('returns valid: true when no missing or invalid fields', () => {
|
|
85
|
-
const result = checkConfig(schema, { host: 'localhost', port: 8080 });
|
|
86
|
-
expect(result.valid).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('returns valid: false when required field is missing', () => {
|
|
90
|
-
const result = checkConfig(schema, { port: 8080 });
|
|
91
|
-
expect(result.valid).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('returns valid: false when field is invalid', () => {
|
|
95
|
-
const result = checkConfig(schema, {
|
|
96
|
-
host: 'localhost',
|
|
97
|
-
port: 'bad',
|
|
98
|
-
});
|
|
99
|
-
expect(result.valid).toBe(false);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('returns valid: true when deprecated fields are present', () => {
|
|
103
|
-
const deprecatedSchema = z.object({
|
|
104
|
-
host: z.string(),
|
|
105
|
-
legacyMode: deprecated(z.boolean(), 'Use "mode" instead').default(
|
|
106
|
-
false
|
|
107
|
-
),
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const result = checkConfig(deprecatedSchema, {
|
|
111
|
-
host: 'localhost',
|
|
112
|
-
legacyMode: true,
|
|
113
|
-
});
|
|
114
|
-
expect(result.valid).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('env resolution', () => {
|
|
119
|
-
test('treats env-provided value as valid', () => {
|
|
120
|
-
const envSchema = z.object({
|
|
121
|
-
host: env(z.string(), 'APP_HOST'),
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const result = checkConfig(
|
|
125
|
-
envSchema,
|
|
126
|
-
{},
|
|
127
|
-
{ env: { APP_HOST: 'envhost' } }
|
|
128
|
-
);
|
|
129
|
-
const hostDiag = result.diagnostics.find((d) => d.path === 'host');
|
|
130
|
-
expect(hostDiag?.status).toBe('valid');
|
|
131
|
-
expect(hostDiag?.value).toBe('envhost');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test('resolves env vars for nested schema fields', () => {
|
|
135
|
-
const nestedSchema = z.object({
|
|
136
|
-
db: z.object({
|
|
137
|
-
host: env(z.string(), 'DB_HOST'),
|
|
138
|
-
port: z.number().default(5432),
|
|
139
|
-
}),
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const result = checkConfig(
|
|
143
|
-
nestedSchema,
|
|
144
|
-
{ db: {} },
|
|
145
|
-
{ env: { DB_HOST: 'dbhost.local' } }
|
|
146
|
-
);
|
|
147
|
-
const dbHostDiag = result.diagnostics.find((d) => d.path === 'db.host');
|
|
148
|
-
expect(dbHostDiag?.status).toBe('valid');
|
|
149
|
-
expect(dbHostDiag?.value).toBe('dbhost.local');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test('walks optional nested object schemas for env overrides', () => {
|
|
153
|
-
const nestedSchema = z.object({
|
|
154
|
-
db: z
|
|
155
|
-
.object({
|
|
156
|
-
host: env(z.string(), 'DB_HOST'),
|
|
157
|
-
})
|
|
158
|
-
.optional(),
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const result = checkConfig(
|
|
162
|
-
nestedSchema,
|
|
163
|
-
{},
|
|
164
|
-
{ env: { DB_HOST: 'dbhost.local' } }
|
|
165
|
-
);
|
|
166
|
-
const dbHostDiag = result.diagnostics.find((d) => d.path === 'db.host');
|
|
167
|
-
|
|
168
|
-
expect(dbHostDiag?.status).toBe('valid');
|
|
169
|
-
expect(dbHostDiag?.value).toBe('dbhost.local');
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
});
|