@ontrails/config 1.0.0-beta.14 → 1.0.0-beta.15
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +20 -28
- package/dist/app-config.d.ts +4 -4
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +12 -7
- package/dist/app-config.js.map +1 -1
- package/dist/compose.d.ts +11 -15
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +7 -9
- package/dist/compose.js.map +1 -1
- package/dist/config-layer.d.ts +3 -3
- package/dist/config-resource.d.ts +3 -0
- package/dist/config-resource.d.ts.map +1 -0
- package/dist/{config-provision.js → config-resource.js} +6 -6
- package/dist/config-resource.js.map +1 -0
- package/dist/define-config.d.ts +9 -9
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js +9 -12
- package/dist/define-config.js.map +1 -1
- package/dist/{generate → derive}/env.d.ts +1 -1
- package/dist/derive/env.d.ts.map +1 -0
- package/dist/{generate → derive}/env.js +3 -3
- package/dist/derive/env.js.map +1 -0
- package/dist/{generate → derive}/example.d.ts +1 -1
- package/dist/derive/example.d.ts.map +1 -0
- package/dist/{generate → derive}/example.js +1 -1
- package/dist/derive/example.js.map +1 -0
- package/dist/{generate → derive}/helpers.d.ts +1 -1
- package/dist/derive/helpers.d.ts.map +1 -0
- package/dist/{generate → derive}/helpers.js +1 -1
- package/dist/derive/helpers.js.map +1 -0
- package/dist/derive/index.d.ts +4 -0
- package/dist/derive/index.d.ts.map +1 -0
- package/dist/derive/index.js +4 -0
- package/dist/derive/index.js.map +1 -0
- package/dist/{generate → derive}/json-schema.d.ts +1 -1
- package/dist/derive/json-schema.d.ts.map +1 -0
- package/dist/{generate → derive}/json-schema.js +1 -1
- package/dist/derive/json-schema.js.map +1 -0
- package/dist/{describe.d.ts → derive-fields.d.ts} +2 -2
- package/dist/derive-fields.d.ts.map +1 -0
- package/dist/{describe.js → derive-fields.js} +2 -2
- package/dist/derive-fields.js.map +1 -0
- package/dist/{explain.d.ts → derive-provenance.d.ts} +5 -5
- package/dist/derive-provenance.d.ts.map +1 -0
- package/dist/{explain.js → derive-provenance.js} +3 -3
- package/dist/derive-provenance.js.map +1 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +2 -2
- package/dist/resolve.d.ts +6 -6
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +7 -7
- package/dist/resolve.js.map +1 -1
- package/dist/trails/config-check.d.ts +1 -1
- package/dist/trails/config-check.d.ts.map +1 -1
- package/dist/trails/config-check.js +3 -3
- package/dist/trails/config-check.js.map +1 -1
- package/dist/trails/config-describe.d.ts +1 -1
- package/dist/trails/config-describe.d.ts.map +1 -1
- package/dist/trails/config-describe.js +5 -5
- package/dist/trails/config-describe.js.map +1 -1
- package/dist/trails/config-explain.d.ts +1 -1
- package/dist/trails/config-explain.d.ts.map +1 -1
- package/dist/trails/config-explain.js +8 -8
- package/dist/trails/config-explain.js.map +1 -1
- package/dist/trails/config-init.d.ts +1 -1
- package/dist/trails/config-init.d.ts.map +1 -1
- package/dist/trails/config-init.js +7 -7
- package/dist/trails/config-init.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/app-config.test.ts +19 -6
- package/src/__tests__/compose.test.ts +15 -15
- package/src/__tests__/config-check.test.ts +7 -7
- package/src/__tests__/config-describe.test.ts +7 -7
- package/src/__tests__/config-explain.test.ts +7 -7
- package/src/__tests__/config-init.test.ts +7 -7
- package/src/__tests__/{config-provision.test.ts → config-resource.test.ts} +12 -12
- package/src/__tests__/define-config.test.ts +24 -18
- package/src/__tests__/{describe.test.ts → derive-fields.test.ts} +11 -11
- package/src/__tests__/{explain.test.ts → derive-provenance.test.ts} +18 -18
- package/src/__tests__/{generate.test.ts → derive.test.ts} +30 -30
- package/src/__tests__/resolve.test.ts +31 -31
- 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 +12 -15
- 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/index.ts +12 -18
- package/src/registry.ts +2 -2
- package/src/resolve.ts +13 -13
- 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/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -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-provision.d.ts +0 -3
- package/dist/config-provision.d.ts.map +0 -1
- 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/describe.d.ts.map +0 -1
- package/dist/describe.js.map +0 -1
- package/dist/explain.d.ts.map +0 -1
- package/dist/explain.js.map +0 -1
- package/dist/generate/env.d.ts.map +0 -1
- package/dist/generate/env.js.map +0 -1
- package/dist/generate/example.d.ts.map +0 -1
- package/dist/generate/example.js.map +0 -1
- package/dist/generate/helpers.d.ts.map +0 -1
- 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.map +0 -1
- package/dist/generate/json-schema.js.map +0 -1
- package/src/__tests__/config-gate.test.ts +0 -53
- package/src/config-gate.ts +0 -15
- package/src/generate/index.ts +0 -3
|
@@ -2,9 +2,9 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
import { deprecated, env, secret } from '../extensions.js';
|
|
5
|
-
import {
|
|
5
|
+
import { deriveConfigFields } from '../derive-fields.js';
|
|
6
6
|
|
|
7
|
-
describe('
|
|
7
|
+
describe('deriveConfigFields', () => {
|
|
8
8
|
describe('basic field descriptions', () => {
|
|
9
9
|
test('returns path, type, and required for each field', () => {
|
|
10
10
|
const schema = z.object({
|
|
@@ -13,7 +13,7 @@ describe('describeConfig', () => {
|
|
|
13
13
|
port: z.number(),
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
const fields =
|
|
16
|
+
const fields = deriveConfigFields(schema);
|
|
17
17
|
|
|
18
18
|
expect(fields).toHaveLength(3);
|
|
19
19
|
const host = fields.find((f) => f.path === 'host');
|
|
@@ -32,7 +32,7 @@ describe('describeConfig', () => {
|
|
|
32
32
|
host: z.string().describe('The server hostname'),
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
const fields =
|
|
35
|
+
const fields = deriveConfigFields(schema);
|
|
36
36
|
const host = fields.find((f) => f.path === 'host');
|
|
37
37
|
expect(host?.description).toBe('The server hostname');
|
|
38
38
|
});
|
|
@@ -47,7 +47,7 @@ describe('describeConfig', () => {
|
|
|
47
47
|
),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
const fields =
|
|
50
|
+
const fields = deriveConfigFields(schema);
|
|
51
51
|
|
|
52
52
|
const apiKey = fields.find((f) => f.path === 'apiKey');
|
|
53
53
|
expect(apiKey?.env).toBe('API_KEY');
|
|
@@ -65,7 +65,7 @@ describe('describeConfig', () => {
|
|
|
65
65
|
port: z.number().default(3000),
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
const fields =
|
|
68
|
+
const fields = deriveConfigFields(schema);
|
|
69
69
|
|
|
70
70
|
const port = fields.find((f) => f.path === 'port');
|
|
71
71
|
expect(port?.default).toBe(3000);
|
|
@@ -85,7 +85,7 @@ describe('describeConfig', () => {
|
|
|
85
85
|
}),
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
const fields =
|
|
88
|
+
const fields = deriveConfigFields(schema);
|
|
89
89
|
|
|
90
90
|
const dbHost = fields.find((f) => f.path === 'db.host');
|
|
91
91
|
expect(dbHost?.type).toBe('string');
|
|
@@ -106,7 +106,7 @@ describe('describeConfig', () => {
|
|
|
106
106
|
.optional(),
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
const fields =
|
|
109
|
+
const fields = deriveConfigFields(schema);
|
|
110
110
|
|
|
111
111
|
expect(fields).toEqual([
|
|
112
112
|
expect.objectContaining({ path: 'db.host', required: true }),
|
|
@@ -121,7 +121,7 @@ describe('describeConfig', () => {
|
|
|
121
121
|
env: z.enum(['development', 'production', 'test']),
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
const fields =
|
|
124
|
+
const fields = deriveConfigFields(schema);
|
|
125
125
|
const envField = fields.find((f) => f.path === 'env');
|
|
126
126
|
expect(envField?.type).toBe('enum');
|
|
127
127
|
expect(envField?.constraints?.values).toEqual([
|
|
@@ -136,7 +136,7 @@ describe('describeConfig', () => {
|
|
|
136
136
|
port: z.number().min(1).max(65_535),
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
const fields =
|
|
139
|
+
const fields = deriveConfigFields(schema);
|
|
140
140
|
const port = fields.find((f) => f.path === 'port');
|
|
141
141
|
expect(port?.constraints?.min).toBe(1);
|
|
142
142
|
expect(port?.constraints?.max).toBe(65_535);
|
|
@@ -150,7 +150,7 @@ describe('describeConfig', () => {
|
|
|
150
150
|
nickname: z.string().optional(),
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
const fields =
|
|
153
|
+
const fields = deriveConfigFields(schema);
|
|
154
154
|
const nickname = fields.find((f) => f.path === 'nickname');
|
|
155
155
|
expect(nickname?.required).toBe(false);
|
|
156
156
|
});
|
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
import { env, secret } from '../extensions.js';
|
|
5
|
-
import {
|
|
5
|
+
import { deriveConfigProvenance } from '../derive-provenance.js';
|
|
6
6
|
|
|
7
7
|
const schema = z.object({
|
|
8
8
|
debug: z.boolean().default(false),
|
|
@@ -10,10 +10,10 @@ const schema = z.object({
|
|
|
10
10
|
port: z.number().default(3000),
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
describe('
|
|
13
|
+
describe('deriveConfigProvenance', () => {
|
|
14
14
|
describe('default source', () => {
|
|
15
15
|
test('reports "default" when no other source provides value', () => {
|
|
16
|
-
const entries =
|
|
16
|
+
const entries = deriveConfigProvenance({
|
|
17
17
|
resolved: { debug: false, host: 'localhost', port: 3000 },
|
|
18
18
|
schema,
|
|
19
19
|
});
|
|
@@ -26,7 +26,7 @@ describe('explainConfig', () => {
|
|
|
26
26
|
|
|
27
27
|
describe('base overrides default', () => {
|
|
28
28
|
test('reports "base" when base provides value', () => {
|
|
29
|
-
const entries =
|
|
29
|
+
const entries = deriveConfigProvenance({
|
|
30
30
|
base: { host: 'base.example.com' },
|
|
31
31
|
resolved: { debug: false, host: 'base.example.com', port: 3000 },
|
|
32
32
|
schema,
|
|
@@ -38,27 +38,27 @@ describe('explainConfig', () => {
|
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
describe('
|
|
42
|
-
test('reports "
|
|
43
|
-
const entries =
|
|
41
|
+
describe('profile overrides base', () => {
|
|
42
|
+
test('reports "profile" when profile provides winning value', () => {
|
|
43
|
+
const entries = deriveConfigProvenance({
|
|
44
44
|
base: { host: 'base.example.com' },
|
|
45
|
-
|
|
46
|
-
resolved: { debug: false, host: '
|
|
45
|
+
profile: { host: 'profile.example.com' },
|
|
46
|
+
resolved: { debug: false, host: 'profile.example.com', port: 3000 },
|
|
47
47
|
schema,
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
const host = entries.find((e) => e.path === 'host');
|
|
51
|
-
expect(host?.source).toBe('
|
|
52
|
-
expect(host?.value).toBe('
|
|
51
|
+
expect(host?.source).toBe('profile');
|
|
52
|
+
expect(host?.value).toBe('profile.example.com');
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
describe('local overrides
|
|
56
|
+
describe('local overrides profile', () => {
|
|
57
57
|
test('reports "local" when local provides winning value', () => {
|
|
58
|
-
const entries =
|
|
58
|
+
const entries = deriveConfigProvenance({
|
|
59
59
|
base: { host: 'base.example.com' },
|
|
60
|
-
loadout: { host: 'loadout.example.com' },
|
|
61
60
|
local: { host: 'local.example.com' },
|
|
61
|
+
profile: { host: 'profile.example.com' },
|
|
62
62
|
resolved: { debug: false, host: 'local.example.com', port: 3000 },
|
|
63
63
|
schema,
|
|
64
64
|
});
|
|
@@ -75,7 +75,7 @@ describe('explainConfig', () => {
|
|
|
75
75
|
port: z.number().default(3000),
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
const entries =
|
|
78
|
+
const entries = deriveConfigProvenance({
|
|
79
79
|
base: { host: 'base.example.com' },
|
|
80
80
|
env: { APP_HOST: 'env.example.com' },
|
|
81
81
|
resolved: { host: 'env.example.com', port: 3000 },
|
|
@@ -96,7 +96,7 @@ describe('explainConfig', () => {
|
|
|
96
96
|
.optional(),
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
const entries =
|
|
99
|
+
const entries = deriveConfigProvenance({
|
|
100
100
|
env: { DB_HOST: 'env.example.com' },
|
|
101
101
|
resolved: { db: { host: 'env.example.com' } },
|
|
102
102
|
schema: envSchema,
|
|
@@ -115,7 +115,7 @@ describe('explainConfig', () => {
|
|
|
115
115
|
host: z.string().default('localhost'),
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
const entries =
|
|
118
|
+
const entries = deriveConfigProvenance({
|
|
119
119
|
env: { API_KEY: 'super-secret-key' },
|
|
120
120
|
resolved: { apiKey: 'super-secret-key', host: 'localhost' },
|
|
121
121
|
schema: secretSchema,
|
|
@@ -127,7 +127,7 @@ describe('explainConfig', () => {
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
test('does not redact non-secret fields', () => {
|
|
130
|
-
const entries =
|
|
130
|
+
const entries = deriveConfigProvenance({
|
|
131
131
|
resolved: { debug: false, host: 'localhost', port: 3000 },
|
|
132
132
|
schema,
|
|
133
133
|
});
|
|
@@ -3,10 +3,10 @@ import { z } from 'zod';
|
|
|
3
3
|
|
|
4
4
|
import { deprecated, env, secret } from '../extensions.js';
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from '../
|
|
6
|
+
deriveConfigEnvExample,
|
|
7
|
+
deriveConfigExample,
|
|
8
|
+
deriveConfigJsonSchema,
|
|
9
|
+
} from '../derive/index.js';
|
|
10
10
|
|
|
11
11
|
/** Shared test schema used across generator tests. */
|
|
12
12
|
const testSchema = z.object({
|
|
@@ -39,10 +39,10 @@ const nestedSchema = z.object({
|
|
|
39
39
|
}),
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
describe('
|
|
42
|
+
describe('deriveConfigExample()', () => {
|
|
43
43
|
describe('TOML format', () => {
|
|
44
44
|
test('produces valid TOML with comments for descriptions', () => {
|
|
45
|
-
const result =
|
|
45
|
+
const result = deriveConfigExample(testSchema, 'toml');
|
|
46
46
|
|
|
47
47
|
expect(result).toContain('# The server hostname');
|
|
48
48
|
expect(result).toContain('host = "localhost"');
|
|
@@ -53,13 +53,13 @@ describe('generateExample()', () => {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
test('annotates deprecated fields in TOML', () => {
|
|
56
|
-
const result =
|
|
56
|
+
const result = deriveConfigExample(annotatedSchema, 'toml');
|
|
57
57
|
|
|
58
58
|
expect(result).toContain('# DEPRECATED: Use newEndpoint instead');
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
test('handles nested objects as TOML sections', () => {
|
|
62
|
-
const result =
|
|
62
|
+
const result = deriveConfigExample(nestedSchema, 'toml');
|
|
63
63
|
|
|
64
64
|
expect(result).toContain('[db]');
|
|
65
65
|
expect(result).toContain('[server]');
|
|
@@ -70,7 +70,7 @@ describe('generateExample()', () => {
|
|
|
70
70
|
|
|
71
71
|
describe('JSON format', () => {
|
|
72
72
|
test('produces valid JSON without comments', () => {
|
|
73
|
-
const result =
|
|
73
|
+
const result = deriveConfigExample(testSchema, 'json');
|
|
74
74
|
const parsed = JSON.parse(result);
|
|
75
75
|
|
|
76
76
|
expect(parsed).toEqual({
|
|
@@ -81,7 +81,7 @@ describe('generateExample()', () => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
test('handles nested objects', () => {
|
|
84
|
-
const result =
|
|
84
|
+
const result = deriveConfigExample(nestedSchema, 'json');
|
|
85
85
|
const parsed = JSON.parse(result);
|
|
86
86
|
|
|
87
87
|
expect(parsed).toHaveProperty('db');
|
|
@@ -92,7 +92,7 @@ describe('generateExample()', () => {
|
|
|
92
92
|
|
|
93
93
|
describe('JSONC format', () => {
|
|
94
94
|
test('produces JSON with // comments for descriptions', () => {
|
|
95
|
-
const result =
|
|
95
|
+
const result = deriveConfigExample(testSchema, 'jsonc');
|
|
96
96
|
|
|
97
97
|
expect(result).toContain('// The server hostname');
|
|
98
98
|
expect(result).toContain('"host"');
|
|
@@ -100,13 +100,13 @@ describe('generateExample()', () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
test('annotates deprecated fields in JSONC', () => {
|
|
103
|
-
const result =
|
|
103
|
+
const result = deriveConfigExample(annotatedSchema, 'jsonc');
|
|
104
104
|
|
|
105
105
|
expect(result).toContain('// DEPRECATED: Use newEndpoint instead');
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
test('handles nested objects', () => {
|
|
109
|
-
const result =
|
|
109
|
+
const result = deriveConfigExample(nestedSchema, 'jsonc');
|
|
110
110
|
|
|
111
111
|
expect(result).toContain('"db"');
|
|
112
112
|
expect(result).toContain('"host"');
|
|
@@ -117,7 +117,7 @@ describe('generateExample()', () => {
|
|
|
117
117
|
|
|
118
118
|
describe('YAML format', () => {
|
|
119
119
|
test('produces valid YAML with comments for descriptions', () => {
|
|
120
|
-
const result =
|
|
120
|
+
const result = deriveConfigExample(testSchema, 'yaml');
|
|
121
121
|
|
|
122
122
|
expect(result).toContain('# The server hostname');
|
|
123
123
|
expect(result).toContain('host: "localhost"');
|
|
@@ -128,13 +128,13 @@ describe('generateExample()', () => {
|
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
test('annotates deprecated fields in YAML', () => {
|
|
131
|
-
const result =
|
|
131
|
+
const result = deriveConfigExample(annotatedSchema, 'yaml');
|
|
132
132
|
|
|
133
133
|
expect(result).toContain('# DEPRECATED: Use newEndpoint instead');
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
test('handles nested objects', () => {
|
|
137
|
-
const result =
|
|
137
|
+
const result = deriveConfigExample(nestedSchema, 'yaml');
|
|
138
138
|
|
|
139
139
|
expect(result).toContain('db:');
|
|
140
140
|
expect(result).toContain(' host: "localhost"');
|
|
@@ -143,9 +143,9 @@ describe('generateExample()', () => {
|
|
|
143
143
|
});
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
describe('
|
|
146
|
+
describe('deriveConfigJsonSchema()', () => {
|
|
147
147
|
test('produces valid JSON Schema with $schema, type, and properties', () => {
|
|
148
|
-
const result =
|
|
148
|
+
const result = deriveConfigJsonSchema(testSchema);
|
|
149
149
|
|
|
150
150
|
expect(result.$schema).toBe('https://json-schema.org/draft/2020-12/schema');
|
|
151
151
|
expect(result.type).toBe('object');
|
|
@@ -153,7 +153,7 @@ describe('generateJsonSchema()', () => {
|
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
test('includes title and description from options', () => {
|
|
156
|
-
const result =
|
|
156
|
+
const result = deriveConfigJsonSchema(testSchema, {
|
|
157
157
|
description: 'Server configuration',
|
|
158
158
|
title: 'ServerConfig',
|
|
159
159
|
});
|
|
@@ -163,7 +163,7 @@ describe('generateJsonSchema()', () => {
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
test('includes descriptions from .describe()', () => {
|
|
166
|
-
const result =
|
|
166
|
+
const result = deriveConfigJsonSchema(testSchema);
|
|
167
167
|
const props = result.properties as Record<string, Record<string, unknown>>;
|
|
168
168
|
|
|
169
169
|
expect(props['host']?.description).toBe('The server hostname');
|
|
@@ -171,7 +171,7 @@ describe('generateJsonSchema()', () => {
|
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
test('includes defaults', () => {
|
|
174
|
-
const result =
|
|
174
|
+
const result = deriveConfigJsonSchema(testSchema);
|
|
175
175
|
const props = result.properties as Record<string, Record<string, unknown>>;
|
|
176
176
|
|
|
177
177
|
expect(props['host']?.default).toBe('localhost');
|
|
@@ -187,7 +187,7 @@ describe('generateJsonSchema()', () => {
|
|
|
187
187
|
name: z.string().describe('A name'),
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
-
const result =
|
|
190
|
+
const result = deriveConfigJsonSchema(enumSchema);
|
|
191
191
|
const props = result.properties as Record<string, Record<string, unknown>>;
|
|
192
192
|
|
|
193
193
|
expect(props['name']?.type).toBe('string');
|
|
@@ -197,21 +197,21 @@ describe('generateJsonSchema()', () => {
|
|
|
197
197
|
});
|
|
198
198
|
|
|
199
199
|
test('marks deprecated fields', () => {
|
|
200
|
-
const result =
|
|
200
|
+
const result = deriveConfigJsonSchema(annotatedSchema);
|
|
201
201
|
const props = result.properties as Record<string, Record<string, unknown>>;
|
|
202
202
|
|
|
203
203
|
expect(props['oldEndpoint']?.deprecated).toBe(true);
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
test('lists required fields (those without defaults or optional)', () => {
|
|
207
|
-
const result =
|
|
207
|
+
const result = deriveConfigJsonSchema(annotatedSchema);
|
|
208
208
|
|
|
209
209
|
expect(result.required).toContain('apiKey');
|
|
210
210
|
expect(result.required).toContain('oldEndpoint');
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
test('recurses into nested object fields', () => {
|
|
214
|
-
const result =
|
|
214
|
+
const result = deriveConfigJsonSchema(nestedSchema);
|
|
215
215
|
const props = result.properties as Record<string, Record<string, unknown>>;
|
|
216
216
|
|
|
217
217
|
expect(props['db']?.type).toBe('object');
|
|
@@ -232,9 +232,9 @@ describe('generateJsonSchema()', () => {
|
|
|
232
232
|
});
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
-
describe('
|
|
235
|
+
describe('deriveConfigEnvExample()', () => {
|
|
236
236
|
test('lists env vars with type info', () => {
|
|
237
|
-
const result =
|
|
237
|
+
const result = deriveConfigEnvExample(testSchema);
|
|
238
238
|
|
|
239
239
|
expect(result).toContain('HOST=');
|
|
240
240
|
expect(result).toContain('PORT=');
|
|
@@ -243,14 +243,14 @@ describe('generateEnvExample()', () => {
|
|
|
243
243
|
});
|
|
244
244
|
|
|
245
245
|
test('annotates secrets', () => {
|
|
246
|
-
const result =
|
|
246
|
+
const result = deriveConfigEnvExample(annotatedSchema);
|
|
247
247
|
|
|
248
248
|
expect(result).toContain('API_KEY=');
|
|
249
249
|
expect(result).toContain('secret');
|
|
250
250
|
});
|
|
251
251
|
|
|
252
252
|
test('shows defaults as comments', () => {
|
|
253
|
-
const result =
|
|
253
|
+
const result = deriveConfigEnvExample(testSchema);
|
|
254
254
|
|
|
255
255
|
expect(result).toContain('default: "localhost"');
|
|
256
256
|
expect(result).toContain('default: 3000');
|
|
@@ -262,7 +262,7 @@ describe('generateEnvExample()', () => {
|
|
|
262
262
|
verbose: z.boolean().default(false),
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
-
const result =
|
|
265
|
+
const result = deriveConfigEnvExample(noEnvSchema);
|
|
266
266
|
|
|
267
267
|
expect(result).toBe('');
|
|
268
268
|
});
|
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
import { env } from '../extensions.js';
|
|
5
|
-
import {
|
|
5
|
+
import { deriveConfig } from '../resolve.js';
|
|
6
6
|
|
|
7
7
|
const baseSchema = z.object({
|
|
8
8
|
debug: z.boolean().default(false),
|
|
@@ -10,10 +10,10 @@ const baseSchema = z.object({
|
|
|
10
10
|
port: z.number().default(3000),
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
describe('
|
|
13
|
+
describe('deriveConfig', () => {
|
|
14
14
|
describe('schema defaults', () => {
|
|
15
15
|
test('applies schema defaults when no other source provides values', () => {
|
|
16
|
-
const result =
|
|
16
|
+
const result = deriveConfig({ schema: baseSchema });
|
|
17
17
|
|
|
18
18
|
expect(result.isOk()).toBe(true);
|
|
19
19
|
expect(result.unwrap()).toEqual({
|
|
@@ -26,7 +26,7 @@ describe('resolveConfig', () => {
|
|
|
26
26
|
|
|
27
27
|
describe('base config', () => {
|
|
28
28
|
test('overrides schema defaults', () => {
|
|
29
|
-
const result =
|
|
29
|
+
const result = deriveConfig({
|
|
30
30
|
base: { host: 'example.com', port: 8080 },
|
|
31
31
|
schema: baseSchema,
|
|
32
32
|
});
|
|
@@ -40,12 +40,12 @@ describe('resolveConfig', () => {
|
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
describe('
|
|
44
|
-
test('overrides base config for matching
|
|
45
|
-
const result =
|
|
43
|
+
describe('profiles', () => {
|
|
44
|
+
test('overrides base config for matching profile', () => {
|
|
45
|
+
const result = deriveConfig({
|
|
46
46
|
base: { host: 'example.com', port: 8080 },
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
profile: 'production',
|
|
48
|
+
profiles: {
|
|
49
49
|
production: { host: 'prod.example.com', port: 443 },
|
|
50
50
|
},
|
|
51
51
|
schema: baseSchema,
|
|
@@ -58,11 +58,11 @@ describe('resolveConfig', () => {
|
|
|
58
58
|
expect(value.debug).toBe(false);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
test('silently ignores unrecognized
|
|
62
|
-
const result =
|
|
61
|
+
test('silently ignores unrecognized profile (base only)', () => {
|
|
62
|
+
const result = deriveConfig({
|
|
63
63
|
base: { host: 'example.com' },
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
profile: 'staging',
|
|
65
|
+
profiles: {
|
|
66
66
|
production: { host: 'prod.example.com' },
|
|
67
67
|
},
|
|
68
68
|
schema: baseSchema,
|
|
@@ -74,7 +74,7 @@ describe('resolveConfig', () => {
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
describe('local overrides', () => {
|
|
77
|
-
test('deep-merge on top of
|
|
77
|
+
test('deep-merge on top of profile', () => {
|
|
78
78
|
const nestedSchema = z.object({
|
|
79
79
|
db: z
|
|
80
80
|
.object({
|
|
@@ -84,13 +84,13 @@ describe('resolveConfig', () => {
|
|
|
84
84
|
.default({}),
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
const result =
|
|
87
|
+
const result = deriveConfig({
|
|
88
88
|
base: { db: { host: 'db.example.com', port: 5432 } },
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
localOverrides: { db: { port: 9999 } },
|
|
90
|
+
profile: 'production',
|
|
91
|
+
profiles: {
|
|
91
92
|
production: { db: { host: 'prod-db.example.com' } },
|
|
92
93
|
},
|
|
93
|
-
localOverrides: { db: { port: 9999 } },
|
|
94
94
|
schema: nestedSchema,
|
|
95
95
|
});
|
|
96
96
|
|
|
@@ -108,7 +108,7 @@ describe('resolveConfig', () => {
|
|
|
108
108
|
port: env(z.number(), 'APP_PORT').default(3000),
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
const result =
|
|
111
|
+
const result = deriveConfig({
|
|
112
112
|
base: { host: 'example.com', port: 8080 },
|
|
113
113
|
env: { APP_HOST: 'env-host.example.com', APP_PORT: '9090' },
|
|
114
114
|
schema,
|
|
@@ -125,7 +125,7 @@ describe('resolveConfig', () => {
|
|
|
125
125
|
port: env(z.number(), 'PORT').default(3000),
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
const result =
|
|
128
|
+
const result = deriveConfig({
|
|
129
129
|
env: { PORT: '8080' },
|
|
130
130
|
schema,
|
|
131
131
|
});
|
|
@@ -139,7 +139,7 @@ describe('resolveConfig', () => {
|
|
|
139
139
|
port: env(z.number(), 'PORT').default(3000),
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
const result =
|
|
142
|
+
const result = deriveConfig({
|
|
143
143
|
env: { PORT: 'abc' },
|
|
144
144
|
schema,
|
|
145
145
|
});
|
|
@@ -153,7 +153,7 @@ describe('resolveConfig', () => {
|
|
|
153
153
|
verbose: env(z.boolean(), 'VERBOSE').default(false),
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
const trueValues =
|
|
156
|
+
const trueValues = deriveConfig({
|
|
157
157
|
env: { DEBUG: 'true', VERBOSE: '1' },
|
|
158
158
|
schema,
|
|
159
159
|
});
|
|
@@ -161,7 +161,7 @@ describe('resolveConfig', () => {
|
|
|
161
161
|
expect(trueValues.unwrap().debug).toBe(true);
|
|
162
162
|
expect(trueValues.unwrap().verbose).toBe(true);
|
|
163
163
|
|
|
164
|
-
const falseValues =
|
|
164
|
+
const falseValues = deriveConfig({
|
|
165
165
|
env: { DEBUG: 'false', VERBOSE: '0' },
|
|
166
166
|
schema,
|
|
167
167
|
});
|
|
@@ -177,7 +177,7 @@ describe('resolveConfig', () => {
|
|
|
177
177
|
required: z.string(),
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
const result =
|
|
180
|
+
const result = deriveConfig({ schema });
|
|
181
181
|
|
|
182
182
|
expect(result.isErr()).toBe(true);
|
|
183
183
|
});
|
|
@@ -190,7 +190,7 @@ describe('resolveConfig', () => {
|
|
|
190
190
|
});
|
|
191
191
|
const base = { host: 'dev-host' };
|
|
192
192
|
|
|
193
|
-
const first =
|
|
193
|
+
const first = deriveConfig({
|
|
194
194
|
base,
|
|
195
195
|
env: { APP_HOST: 'env-host' },
|
|
196
196
|
schema,
|
|
@@ -198,7 +198,7 @@ describe('resolveConfig', () => {
|
|
|
198
198
|
expect(first.isOk()).toBe(true);
|
|
199
199
|
expect(first.unwrap().host).toBe('env-host');
|
|
200
200
|
|
|
201
|
-
const second =
|
|
201
|
+
const second = deriveConfig({ base, schema });
|
|
202
202
|
expect(second.isOk()).toBe(true);
|
|
203
203
|
expect(second.unwrap().host).toBe('dev-host');
|
|
204
204
|
expect(base.host).toBe('dev-host');
|
|
@@ -215,16 +215,16 @@ describe('resolveConfig', () => {
|
|
|
215
215
|
port: z.number().default(3000),
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
const result =
|
|
218
|
+
const result = deriveConfig({
|
|
219
219
|
// base overrides name
|
|
220
220
|
base: { name: 'my-app', port: 8080 },
|
|
221
221
|
// env overrides debug and apiUrl
|
|
222
222
|
env: { API_URL: 'https://api.prod.com', DEBUG: 'true' },
|
|
223
|
-
// loadout overrides port
|
|
224
|
-
loadout: 'production',
|
|
225
|
-
loadouts: { production: { port: 443 } },
|
|
226
223
|
// local overrides local
|
|
227
224
|
localOverrides: { local: 'my-local-value' },
|
|
225
|
+
// profile overrides port
|
|
226
|
+
profile: 'production',
|
|
227
|
+
profiles: { production: { port: 443 } },
|
|
228
228
|
schema,
|
|
229
229
|
});
|
|
230
230
|
|
|
@@ -234,7 +234,7 @@ describe('resolveConfig', () => {
|
|
|
234
234
|
// (nothing left at just default — name was overridden by base)
|
|
235
235
|
// Base
|
|
236
236
|
expect(value.name).toBe('my-app');
|
|
237
|
-
//
|
|
237
|
+
// Profile overrides base port
|
|
238
238
|
expect(value.port).toBe(443);
|
|
239
239
|
// Local overrides
|
|
240
240
|
expect(value.local).toBe('my-local-value');
|
package/src/app-config.ts
CHANGED
|
@@ -9,10 +9,13 @@ import type { z } from 'zod';
|
|
|
9
9
|
|
|
10
10
|
import type { CheckResult } from './doctor.js';
|
|
11
11
|
import { checkConfig } from './doctor.js';
|
|
12
|
-
import type { FieldDescription } from './
|
|
13
|
-
import {
|
|
14
|
-
import type {
|
|
15
|
-
|
|
12
|
+
import type { FieldDescription } from './derive-fields.js';
|
|
13
|
+
import { deriveConfigFields } from './derive-fields.js';
|
|
14
|
+
import type {
|
|
15
|
+
DeriveConfigProvenanceOptions,
|
|
16
|
+
ProvenanceEntry,
|
|
17
|
+
} from './derive-provenance.js';
|
|
18
|
+
import { deriveConfigProvenance } from './derive-provenance.js';
|
|
16
19
|
import type { ConfigRef } from './ref.js';
|
|
17
20
|
import { configRef } from './ref.js';
|
|
18
21
|
|
|
@@ -39,8 +42,8 @@ export interface ResolveOptions {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/** Options for the `explain()` method on AppConfig, excluding schema. */
|
|
42
|
-
export type
|
|
43
|
-
|
|
45
|
+
export type AppConfigDeriveProvenanceOptions = Omit<
|
|
46
|
+
DeriveConfigProvenanceOptions<z.ZodType>,
|
|
44
47
|
'schema'
|
|
45
48
|
>;
|
|
46
49
|
|
|
@@ -62,7 +65,9 @@ export interface AppConfig<T extends z.ZodType> {
|
|
|
62
65
|
): CheckResult;
|
|
63
66
|
|
|
64
67
|
/** Show which source won for each config field. */
|
|
65
|
-
explain(
|
|
68
|
+
explain(
|
|
69
|
+
options: AppConfigDeriveProvenanceOptions
|
|
70
|
+
): readonly ProvenanceEntry[];
|
|
66
71
|
|
|
67
72
|
/** Create a lazy reference to a config field for use as a trail input default. */
|
|
68
73
|
ref(fieldPath: string): ConfigRef;
|
|
@@ -72,7 +77,12 @@ export interface AppConfig<T extends z.ZodType> {
|
|
|
72
77
|
// Default values
|
|
73
78
|
// ---------------------------------------------------------------------------
|
|
74
79
|
|
|
75
|
-
const DEFAULT_FORMATS: readonly ConfigFormat[] = [
|
|
80
|
+
const DEFAULT_FORMATS: readonly ConfigFormat[] = [
|
|
81
|
+
'toml',
|
|
82
|
+
'json',
|
|
83
|
+
'jsonc',
|
|
84
|
+
'yaml',
|
|
85
|
+
];
|
|
76
86
|
|
|
77
87
|
// ---------------------------------------------------------------------------
|
|
78
88
|
// Internal helpers (defined before consumers — no-use-before-define)
|
|
@@ -225,7 +235,7 @@ const discoverConfigFile = async (
|
|
|
225
235
|
};
|
|
226
236
|
|
|
227
237
|
/** Resolve a config file — either from an explicit path or via discovery. */
|
|
228
|
-
const
|
|
238
|
+
const resolveAppConfigFile = async <T extends z.ZodType>(
|
|
229
239
|
name: string,
|
|
230
240
|
schema: T,
|
|
231
241
|
formats: readonly ConfigFormat[],
|
|
@@ -292,16 +302,17 @@ export const appConfig = <T extends z.ZodType>(
|
|
|
292
302
|
return {
|
|
293
303
|
check: (values, checkOpts) => checkConfig(schema, values, checkOpts),
|
|
294
304
|
describe: () =>
|
|
295
|
-
|
|
305
|
+
deriveConfigFields(
|
|
296
306
|
schema as unknown as z.ZodObject<Record<string, z.ZodType>>
|
|
297
307
|
),
|
|
298
308
|
dotfile,
|
|
299
|
-
explain: (explainOpts) =>
|
|
309
|
+
explain: (explainOpts) =>
|
|
310
|
+
deriveConfigProvenance({ ...explainOpts, schema }),
|
|
300
311
|
formats,
|
|
301
312
|
name,
|
|
302
313
|
ref: (fieldPath) => configRef(fieldPath),
|
|
303
314
|
resolve: (resolveOptions?: ResolveOptions) =>
|
|
304
|
-
|
|
315
|
+
resolveAppConfigFile(name, schema, formats, dotfile, resolveOptions),
|
|
305
316
|
schema,
|
|
306
317
|
};
|
|
307
318
|
};
|