@ontrails/config 1.0.0-beta.13 → 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 +18 -0
- package/README.md +204 -0
- 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 -11
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +7 -7
- 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-service.js → config-resource.js} +7 -7
- 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 +21 -19
- 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 +2 -2
- 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} +8 -21
- package/dist/derive-provenance.js.map +1 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +1 -37
- package/dist/doctor.js.map +1 -1
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- 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/ref.d.ts +8 -3
- package/dist/ref.d.ts.map +1 -1
- package/dist/ref.js +8 -3
- package/dist/ref.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 +11 -8
- 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 +12 -12
- 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 +9 -9
- 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 +15 -15
- 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 +16 -16
- package/dist/trails/config-init.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +5 -0
- package/dist/workspace.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/src/workspace.ts +5 -0
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/config-service.d.ts +0 -3
- package/dist/config-service.d.ts.map +0 -1
- 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
|
@@ -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
|
};
|
package/src/compose.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config composition utilities for
|
|
2
|
+
* Config composition utilities for resources.
|
|
3
3
|
*
|
|
4
|
-
* Collects config schemas from
|
|
4
|
+
* Collects config schemas from resource declarations so they can be
|
|
5
5
|
* composed into a unified config structure via `defineConfig`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -11,17 +11,14 @@ import type { z } from 'zod';
|
|
|
11
11
|
// Types
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
|
|
14
|
-
/** A
|
|
15
|
-
export interface
|
|
16
|
-
readonly
|
|
14
|
+
/** A resource config schema entry extracted from a resource declaration. */
|
|
15
|
+
export interface ResourceConfigEntry {
|
|
16
|
+
readonly resourceId: string;
|
|
17
17
|
readonly schema: z.ZodType;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/** Minimal shape needed to extract config from a provision-like object. */
|
|
24
|
-
interface ProvisionWithOptionalConfig {
|
|
20
|
+
/** Minimal shape needed to extract config from a resource-like object. */
|
|
21
|
+
interface ResourceWithOptionalConfig {
|
|
25
22
|
readonly id: string;
|
|
26
23
|
readonly config?: z.ZodType | undefined;
|
|
27
24
|
}
|
|
@@ -31,22 +28,19 @@ interface ProvisionWithOptionalConfig {
|
|
|
31
28
|
// ---------------------------------------------------------------------------
|
|
32
29
|
|
|
33
30
|
/**
|
|
34
|
-
* Collect config schemas from
|
|
31
|
+
* Collect config schemas from resources that declare them.
|
|
35
32
|
*
|
|
36
|
-
* Returns entries keyed by
|
|
37
|
-
*
|
|
33
|
+
* Returns entries keyed by resource ID for composition into `defineConfig`.
|
|
34
|
+
* Resources without a `config` schema are excluded.
|
|
38
35
|
*/
|
|
39
|
-
export const
|
|
40
|
-
|
|
41
|
-
):
|
|
42
|
-
|
|
36
|
+
export const collectResourceConfigs = (
|
|
37
|
+
resources: readonly ResourceWithOptionalConfig[]
|
|
38
|
+
): ResourceConfigEntry[] =>
|
|
39
|
+
resources
|
|
43
40
|
.filter(
|
|
44
41
|
(
|
|
45
42
|
svc
|
|
46
|
-
): svc is
|
|
43
|
+
): svc is ResourceWithOptionalConfig & { readonly config: z.ZodType } =>
|
|
47
44
|
svc.config !== undefined
|
|
48
45
|
)
|
|
49
|
-
.map((svc) => ({
|
|
50
|
-
|
|
51
|
-
/** Backward-compatible alias while the migration is in flight. */
|
|
52
|
-
export const collectServiceConfigs = collectProvisionConfigs;
|
|
46
|
+
.map((svc) => ({ resourceId: svc.id, schema: svc.config }));
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config
|
|
2
|
+
* Config resource — manages resolved config lifecycle.
|
|
3
3
|
*
|
|
4
4
|
* The config is resolved during bootstrap (two-phase init per ADR-010)
|
|
5
|
-
* and registered via `registerConfigState`. This
|
|
6
|
-
* global registry so trails can access it through `
|
|
5
|
+
* and registered via `registerConfigState`. This resource reads from the
|
|
6
|
+
* global registry so trails can access it through `configResource.from(ctx)`.
|
|
7
7
|
*/
|
|
8
|
-
import { InternalError, Result,
|
|
8
|
+
import { InternalError, Result, resource } from '@ontrails/core';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
|
|
11
11
|
import type { ConfigState } from './registry.js';
|
|
12
12
|
import { getConfigState } from './registry.js';
|
|
13
13
|
|
|
14
|
-
export const
|
|
14
|
+
export const configResource = resource<ConfigState>('config', {
|
|
15
15
|
create: () => {
|
|
16
16
|
const state = getConfigState();
|
|
17
17
|
if (state === undefined) {
|
package/src/define-config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Trails-specific config wrapper — `appConfig('trails', ...)` with
|
|
3
|
-
* framework conventions for
|
|
3
|
+
* framework conventions for profile selection and local overrides.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
@@ -9,7 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import type { z } from 'zod';
|
|
10
10
|
|
|
11
11
|
import { appConfig } from './app-config.js';
|
|
12
|
-
import {
|
|
12
|
+
import { deriveConfig } from './resolve.js';
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Types
|
|
@@ -19,14 +19,14 @@ import { resolveConfig } from './resolve.js';
|
|
|
19
19
|
export interface DefineConfigOptions<T extends z.ZodType> {
|
|
20
20
|
readonly schema: T;
|
|
21
21
|
readonly base?: Partial<z.infer<T>>;
|
|
22
|
-
readonly
|
|
22
|
+
readonly profiles?: Record<string, Partial<z.infer<T>>>;
|
|
23
23
|
/** When true, fall back to `NODE_ENV` when `TRAILS_ENV` is unset. */
|
|
24
24
|
readonly envFromNodeEnv?: boolean;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** Options passed to `resolve()` on a defined config. */
|
|
28
28
|
interface DefineConfigResolveOptions {
|
|
29
|
-
readonly
|
|
29
|
+
readonly profile?: string;
|
|
30
30
|
readonly env?: Record<string, string | undefined>;
|
|
31
31
|
/** Working directory for local overrides discovery. Defaults to `process.cwd()`. */
|
|
32
32
|
readonly cwd?: string;
|
|
@@ -70,7 +70,7 @@ const discoverLocalOverrides = async (
|
|
|
70
70
|
* Define Trails app config.
|
|
71
71
|
*
|
|
72
72
|
* This is `appConfig('trails', ...)` with the framework's own conventions:
|
|
73
|
-
* `TRAILS_ENV` selects the
|
|
73
|
+
* `TRAILS_ENV` selects the profile. When `envFromNodeEnv` is true,
|
|
74
74
|
* `NODE_ENV` is used as a fallback when `TRAILS_ENV` is unset.
|
|
75
75
|
*
|
|
76
76
|
* @example
|
|
@@ -81,7 +81,7 @@ const discoverLocalOverrides = async (
|
|
|
81
81
|
* debug: z.boolean().default(false),
|
|
82
82
|
* }),
|
|
83
83
|
* base: { port: 8080 },
|
|
84
|
-
*
|
|
84
|
+
* profiles: {
|
|
85
85
|
* production: { debug: false },
|
|
86
86
|
* test: { debug: true, port: 0 },
|
|
87
87
|
* },
|
|
@@ -93,15 +93,12 @@ const discoverLocalOverrides = async (
|
|
|
93
93
|
export const defineConfig = <T extends z.ZodType>(
|
|
94
94
|
options: DefineConfigOptions<T>
|
|
95
95
|
) => {
|
|
96
|
-
const config = appConfig('trails', {
|
|
97
|
-
formats: ['toml', 'json'],
|
|
98
|
-
schema: options.schema,
|
|
99
|
-
});
|
|
96
|
+
const config = appConfig('trails', { schema: options.schema });
|
|
100
97
|
|
|
101
98
|
return {
|
|
102
99
|
...config,
|
|
103
100
|
base: options.base,
|
|
104
|
-
|
|
101
|
+
profiles: options.profiles,
|
|
105
102
|
resolve: async (resolveOpts?: DefineConfigResolveOptions) => {
|
|
106
103
|
const envRecord = {
|
|
107
104
|
...(resolveOpts?.env ?? process.env),
|
|
@@ -118,14 +115,14 @@ export const defineConfig = <T extends z.ZodType>(
|
|
|
118
115
|
const cwd = resolveOpts?.cwd ?? process.cwd();
|
|
119
116
|
const localOverrides = await discoverLocalOverrides(cwd, envRecord);
|
|
120
117
|
|
|
121
|
-
return
|
|
118
|
+
return deriveConfig({
|
|
122
119
|
base: options.base as Record<string, unknown> | undefined,
|
|
123
120
|
env: envRecord,
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
localOverrides,
|
|
122
|
+
profile: resolveOpts?.profile ?? envRecord['TRAILS_ENV'],
|
|
123
|
+
profiles: options.profiles as
|
|
126
124
|
| Record<string, Record<string, unknown>>
|
|
127
125
|
| undefined,
|
|
128
|
-
localOverrides,
|
|
129
126
|
schema: options.schema,
|
|
130
127
|
});
|
|
131
128
|
},
|