@lorion-org/runtime-config 1.0.0-beta.0 → 1.0.0-beta.5
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/LICENSE +21 -21
- package/README.md +61 -66
- package/dist/index.cjs +85 -0
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +68 -0
- package/package.json +11 -3
- package/src/env.ts +91 -0
- package/src/fragment.ts +28 -0
- package/src/index.ts +8 -0
- package/src/key.ts +54 -0
- package/src/project.ts +208 -0
- package/src/resolve.ts +51 -0
- package/src/scope.ts +129 -0
- package/src/types.ts +34 -0
- package/src/validation.ts +126 -0
- package/examples/context-input-key.ts +0 -46
- package/examples/env-vars.ts +0 -25
- package/examples/namespaced-runtime-config.ts +0 -50
- package/examples/scope-view.ts +0 -32
- package/examples/sectioned-runtime-config.ts +0 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lorion-org/runtime-config",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
4
|
"description": "Framework-free runtime-config types and merge helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"exports": {
|
|
24
24
|
".": {
|
|
25
|
+
"lorion-source": {
|
|
26
|
+
"types": "./src/index.ts",
|
|
27
|
+
"default": "./src/index.ts"
|
|
28
|
+
},
|
|
25
29
|
"import": {
|
|
26
30
|
"types": "./dist/index.d.ts",
|
|
27
31
|
"default": "./dist/index.js"
|
|
@@ -34,8 +38,9 @@
|
|
|
34
38
|
},
|
|
35
39
|
"files": [
|
|
36
40
|
"dist",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"src/**/*.ts",
|
|
43
|
+
"!src/**/*.spec.ts"
|
|
39
44
|
],
|
|
40
45
|
"keywords": [
|
|
41
46
|
"runtime-config",
|
|
@@ -46,6 +51,9 @@
|
|
|
46
51
|
"engines": {
|
|
47
52
|
"node": "^20.19.0 || >=22.12.0"
|
|
48
53
|
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"ajv": "^8.17.1"
|
|
56
|
+
},
|
|
49
57
|
"scripts": {
|
|
50
58
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
51
59
|
"clean": "rimraf coverage dist tsconfig.tsbuildinfo",
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { toSnakeUpperCase } from './key';
|
|
2
|
+
import {
|
|
3
|
+
projectSectionedRuntimeConfig,
|
|
4
|
+
type ProjectSectionedRuntimeConfigOptions,
|
|
5
|
+
} from './project';
|
|
6
|
+
import {
|
|
7
|
+
runtimeConfigVisibilities,
|
|
8
|
+
type ConfigVisibility,
|
|
9
|
+
type NamedRuntimeConfigFragment,
|
|
10
|
+
type RuntimeConfigFragmentMap,
|
|
11
|
+
type RuntimeEnvVars,
|
|
12
|
+
type SectionedRuntimeConfig,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
export type ProjectRuntimeConfigEnvVarsOptions = ProjectSectionedRuntimeConfigOptions & {
|
|
16
|
+
prefix?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function createRuntimeConfigEnvKey(input: {
|
|
20
|
+
key: string;
|
|
21
|
+
prefix?: string;
|
|
22
|
+
scopeId?: string;
|
|
23
|
+
visibility?: ConfigVisibility;
|
|
24
|
+
}): string {
|
|
25
|
+
return toSnakeUpperCase(
|
|
26
|
+
[input.prefix ?? 'APP', input.visibility, input.scopeId, input.key].filter(Boolean).join('_'),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toRuntimeEnvVars(
|
|
31
|
+
runtimeConfig: SectionedRuntimeConfig,
|
|
32
|
+
prefix = 'APP',
|
|
33
|
+
): RuntimeEnvVars {
|
|
34
|
+
const envVars: RuntimeEnvVars = {};
|
|
35
|
+
|
|
36
|
+
for (const visibility of runtimeConfigVisibilities) {
|
|
37
|
+
for (const [key, value] of Object.entries(runtimeConfig[visibility])) {
|
|
38
|
+
if (key.startsWith('__')) continue;
|
|
39
|
+
|
|
40
|
+
envVars[createRuntimeConfigEnvKey({ prefix, visibility, key })] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return envVars;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function projectRuntimeConfigEnvVars(
|
|
48
|
+
fragments: Iterable<NamedRuntimeConfigFragment> | RuntimeConfigFragmentMap,
|
|
49
|
+
options: ProjectRuntimeConfigEnvVarsOptions = {},
|
|
50
|
+
): RuntimeEnvVars {
|
|
51
|
+
const { prefix = 'APP', ...projectOptions } = options;
|
|
52
|
+
|
|
53
|
+
return toRuntimeEnvVars(
|
|
54
|
+
projectSectionedRuntimeConfig(fragments, {
|
|
55
|
+
...projectOptions,
|
|
56
|
+
includeContexts: false,
|
|
57
|
+
}),
|
|
58
|
+
prefix,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function injectRuntimeEnvVars(
|
|
63
|
+
envVars: RuntimeEnvVars,
|
|
64
|
+
processEnv: Record<string, unknown>,
|
|
65
|
+
): RuntimeEnvVars {
|
|
66
|
+
const items: RuntimeEnvVars = {};
|
|
67
|
+
|
|
68
|
+
for (const key of Object.keys(envVars)) {
|
|
69
|
+
items[key] = processEnv[key] ?? envVars[key];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return items;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function runtimeEnvVarsToString(envVars: RuntimeEnvVars): string {
|
|
76
|
+
return Object.entries(envVars)
|
|
77
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
78
|
+
.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function runtimeEnvValueToShellLiteral(value: unknown): string {
|
|
82
|
+
const json = JSON.stringify(value) ?? 'undefined';
|
|
83
|
+
|
|
84
|
+
return `'${json.replace(/'/g, "'\\''")}'`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function runtimeEnvVarsToShellAssignments(envVars: RuntimeEnvVars): string {
|
|
88
|
+
return Object.entries(envVars)
|
|
89
|
+
.map(([key, value]) => `${key}=${runtimeEnvValueToShellLiteral(value)}`)
|
|
90
|
+
.join('\n');
|
|
91
|
+
}
|
package/src/fragment.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RuntimeConfigContext,
|
|
3
|
+
RuntimeConfigFragment,
|
|
4
|
+
RuntimeConfigFragmentInput,
|
|
5
|
+
} from './types';
|
|
6
|
+
|
|
7
|
+
export type NormalizeRuntimeConfigFragmentOptions = {
|
|
8
|
+
contextInputKey?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const isObject = (value: unknown): value is Record<string, unknown> => {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function toRuntimeConfigFragment(
|
|
16
|
+
config: RuntimeConfigFragmentInput,
|
|
17
|
+
options: NormalizeRuntimeConfigFragmentOptions = {},
|
|
18
|
+
): RuntimeConfigFragment {
|
|
19
|
+
const contextInputKey = options.contextInputKey ?? 'contexts';
|
|
20
|
+
const contexts =
|
|
21
|
+
contextInputKey === 'contexts' ? config.contexts : (config.contexts ?? config[contextInputKey]);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...(isObject(config.public) ? { public: config.public } : {}),
|
|
25
|
+
...(isObject(config.private) ? { private: config.private } : {}),
|
|
26
|
+
...(isObject(contexts) ? { contexts: contexts as Record<string, RuntimeConfigContext> } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/index.ts
ADDED
package/src/key.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type RuntimeConfigKeyStrategy = (scopeId: string, key: string) => string;
|
|
2
|
+
|
|
3
|
+
export type RuntimeConfigKeyOptions = {
|
|
4
|
+
keyStrategy?: RuntimeConfigKeyStrategy;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function upperFirst(input: string): string {
|
|
8
|
+
return input ? input[0]!.toUpperCase() + input.slice(1) : input;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function uncapitalize(input: string): string {
|
|
12
|
+
return input ? input[0]!.toLowerCase() + input.slice(1) : input;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function camelCase(input: string): string {
|
|
16
|
+
return input
|
|
17
|
+
.replace(/['\u2019]/g, '')
|
|
18
|
+
.split(/[^A-Za-z0-9]+|(?=[A-Z][a-z])/)
|
|
19
|
+
.map((part) => part.trim())
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.map((part) => part.toLowerCase())
|
|
22
|
+
.map((part, index) => (index === 0 ? part : upperFirst(part)))
|
|
23
|
+
.join('');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createRuntimeConfigKey(
|
|
27
|
+
scopeId: string,
|
|
28
|
+
key: string,
|
|
29
|
+
options: RuntimeConfigKeyOptions = {},
|
|
30
|
+
): string {
|
|
31
|
+
return options.keyStrategy?.(scopeId, key) ?? camelCase(`${scopeId} ${key}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function stripRuntimeConfigScopePrefix(
|
|
35
|
+
scopeId: string,
|
|
36
|
+
configKey: string,
|
|
37
|
+
options: RuntimeConfigKeyOptions = {},
|
|
38
|
+
): string | undefined {
|
|
39
|
+
const prefix = createRuntimeConfigKey(scopeId, '', options);
|
|
40
|
+
if (!prefix || !configKey.startsWith(prefix) || configKey.length <= prefix.length) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return uncapitalize(configKey.slice(prefix.length));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function toSnakeUpperCase(input: string): string {
|
|
48
|
+
return input
|
|
49
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
50
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
51
|
+
.replace(/_+/g, '_')
|
|
52
|
+
.replace(/^_|_$/g, '')
|
|
53
|
+
.toUpperCase();
|
|
54
|
+
}
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createRuntimeConfigKey, type RuntimeConfigKeyOptions } from './key';
|
|
2
|
+
import { toRuntimeConfigFragment, type NormalizeRuntimeConfigFragmentOptions } from './fragment';
|
|
3
|
+
import {
|
|
4
|
+
runtimeConfigVisibilities,
|
|
5
|
+
type NamedRuntimeConfigFragment,
|
|
6
|
+
type RuntimeConfigContext,
|
|
7
|
+
type RuntimeConfigFragment,
|
|
8
|
+
type RuntimeConfigFragmentMap,
|
|
9
|
+
type RuntimeConfigNamespaceProjection,
|
|
10
|
+
type RuntimeConfigSection,
|
|
11
|
+
type SectionedRuntimeConfig,
|
|
12
|
+
type ConfigVisibility,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
export type ProjectSectionedRuntimeConfigOptions = RuntimeConfigKeyOptions &
|
|
16
|
+
NormalizeRuntimeConfigFragmentOptions & {
|
|
17
|
+
contextOutputKey?: string;
|
|
18
|
+
includeContexts?: boolean;
|
|
19
|
+
scopeIds?: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ProjectRuntimeConfigFragmentOptions = Omit<
|
|
23
|
+
ProjectSectionedRuntimeConfigOptions,
|
|
24
|
+
'scopeIds'
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
export type ProjectRuntimeConfigNamespacesOptions = {
|
|
28
|
+
contextInputKey?: string;
|
|
29
|
+
contextId?: string;
|
|
30
|
+
namespaceStrategy?: 'nested' | 'flat';
|
|
31
|
+
scopeIds?: string[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ProjectRuntimeConfigNamespaceOptions = Omit<
|
|
35
|
+
ProjectRuntimeConfigNamespacesOptions,
|
|
36
|
+
'scopeIds'
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
export function normalizeRuntimeConfigFragments(
|
|
40
|
+
fragments: Iterable<NamedRuntimeConfigFragment> | RuntimeConfigFragmentMap,
|
|
41
|
+
options: NormalizeRuntimeConfigFragmentOptions = {},
|
|
42
|
+
): NamedRuntimeConfigFragment[] {
|
|
43
|
+
const entries =
|
|
44
|
+
fragments instanceof Map
|
|
45
|
+
? Array.from(fragments.entries()).map(([scopeId, config]) => ({ scopeId, config }))
|
|
46
|
+
: Array.from(fragments);
|
|
47
|
+
|
|
48
|
+
return entries
|
|
49
|
+
.map((fragment) => ({
|
|
50
|
+
scopeId: fragment.scopeId.trim(),
|
|
51
|
+
config: toRuntimeConfigFragment(fragment.config, options),
|
|
52
|
+
}))
|
|
53
|
+
.filter((fragment) => fragment.scopeId.length > 0)
|
|
54
|
+
.sort((left, right) => left.scopeId.localeCompare(right.scopeId));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function filterFragments(input: {
|
|
58
|
+
fragments: Iterable<NamedRuntimeConfigFragment> | RuntimeConfigFragmentMap;
|
|
59
|
+
contextInputKey?: string;
|
|
60
|
+
scopeIds?: string[];
|
|
61
|
+
}): NamedRuntimeConfigFragment[] {
|
|
62
|
+
const scopeIdSet = input.scopeIds
|
|
63
|
+
? new Set(input.scopeIds.map((id) => id.trim()).filter(Boolean))
|
|
64
|
+
: undefined;
|
|
65
|
+
|
|
66
|
+
return normalizeRuntimeConfigFragments(input.fragments, {
|
|
67
|
+
...(input.contextInputKey ? { contextInputKey: input.contextInputKey } : {}),
|
|
68
|
+
}).filter((fragment) => !scopeIdSet || scopeIdSet.has(fragment.scopeId));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getSection(
|
|
72
|
+
config: RuntimeConfigContext | undefined,
|
|
73
|
+
visibility: ConfigVisibility,
|
|
74
|
+
): RuntimeConfigSection | undefined {
|
|
75
|
+
return config?.[visibility];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function assignDefined(target: RuntimeConfigSection, key: string, value: unknown): void {
|
|
79
|
+
if (value !== undefined) {
|
|
80
|
+
target[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function projectSectionedRuntimeConfig(
|
|
85
|
+
fragments: Iterable<NamedRuntimeConfigFragment> | RuntimeConfigFragmentMap,
|
|
86
|
+
options: ProjectSectionedRuntimeConfigOptions = {},
|
|
87
|
+
): SectionedRuntimeConfig {
|
|
88
|
+
const result: SectionedRuntimeConfig = {
|
|
89
|
+
public: {},
|
|
90
|
+
private: {},
|
|
91
|
+
};
|
|
92
|
+
const contextOutputKey = options.contextOutputKey ?? '__contexts';
|
|
93
|
+
|
|
94
|
+
const filteredFragments = filterFragments({
|
|
95
|
+
fragments,
|
|
96
|
+
...(options.contextInputKey ? { contextInputKey: options.contextInputKey } : {}),
|
|
97
|
+
...(options.scopeIds ? { scopeIds: options.scopeIds } : {}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
for (const fragment of filteredFragments) {
|
|
101
|
+
for (const visibility of runtimeConfigVisibilities) {
|
|
102
|
+
const section = getSection(fragment.config, visibility);
|
|
103
|
+
if (!section) continue;
|
|
104
|
+
|
|
105
|
+
for (const [key, value] of Object.entries(section)) {
|
|
106
|
+
assignDefined(
|
|
107
|
+
result[visibility],
|
|
108
|
+
createRuntimeConfigKey(fragment.scopeId, key, options),
|
|
109
|
+
value,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.includeContexts === false) continue;
|
|
115
|
+
|
|
116
|
+
for (const [contextId, context] of Object.entries(fragment.config.contexts ?? {})) {
|
|
117
|
+
for (const visibility of runtimeConfigVisibilities) {
|
|
118
|
+
const section = getSection(context, visibility);
|
|
119
|
+
if (!section) continue;
|
|
120
|
+
|
|
121
|
+
const contexts =
|
|
122
|
+
(result[visibility][contextOutputKey] as
|
|
123
|
+
| Record<string, RuntimeConfigSection>
|
|
124
|
+
| undefined) ?? {};
|
|
125
|
+
const contextValues = contexts[contextId] ?? {};
|
|
126
|
+
|
|
127
|
+
for (const [key, value] of Object.entries(section)) {
|
|
128
|
+
assignDefined(
|
|
129
|
+
contextValues,
|
|
130
|
+
createRuntimeConfigKey(fragment.scopeId, key, options),
|
|
131
|
+
value,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
contexts[contextId] = contextValues;
|
|
136
|
+
result[visibility][contextOutputKey] = contexts;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function projectRuntimeConfigFragment(
|
|
145
|
+
scopeId: string,
|
|
146
|
+
config: RuntimeConfigFragment,
|
|
147
|
+
options: ProjectRuntimeConfigFragmentOptions = {},
|
|
148
|
+
): SectionedRuntimeConfig {
|
|
149
|
+
return projectSectionedRuntimeConfig(new Map([[scopeId, config]]), options);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function projectRuntimeConfigNamespace(
|
|
153
|
+
scopeId: string,
|
|
154
|
+
config: RuntimeConfigFragment,
|
|
155
|
+
options: ProjectRuntimeConfigNamespaceOptions = {},
|
|
156
|
+
): RuntimeConfigNamespaceProjection {
|
|
157
|
+
return projectRuntimeConfigNamespaces([{ scopeId, config }], options);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function projectRuntimeConfigNamespaces(
|
|
161
|
+
fragments: Iterable<NamedRuntimeConfigFragment> | RuntimeConfigFragmentMap,
|
|
162
|
+
options: ProjectRuntimeConfigNamespacesOptions = {},
|
|
163
|
+
): RuntimeConfigNamespaceProjection {
|
|
164
|
+
const result: RuntimeConfigNamespaceProjection = {
|
|
165
|
+
public: {},
|
|
166
|
+
};
|
|
167
|
+
const namespaceStrategy = options.namespaceStrategy ?? 'nested';
|
|
168
|
+
|
|
169
|
+
const filteredFragments = filterFragments({
|
|
170
|
+
fragments,
|
|
171
|
+
...(options.contextInputKey ? { contextInputKey: options.contextInputKey } : {}),
|
|
172
|
+
...(options.scopeIds ? { scopeIds: options.scopeIds } : {}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
for (const fragment of filteredFragments) {
|
|
176
|
+
const context = options.contextId ? fragment.config.contexts?.[options.contextId] : undefined;
|
|
177
|
+
const publicSection = {
|
|
178
|
+
...(fragment.config.public ?? {}),
|
|
179
|
+
...(context?.public ?? {}),
|
|
180
|
+
};
|
|
181
|
+
const privateSection = {
|
|
182
|
+
...(fragment.config.private ?? {}),
|
|
183
|
+
...(context?.private ?? {}),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (namespaceStrategy === 'flat') {
|
|
187
|
+
Object.assign(result.public, publicSection);
|
|
188
|
+
Object.assign(result, privateSection);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (Object.keys(publicSection).length) {
|
|
193
|
+
result.public[fragment.scopeId] = {
|
|
194
|
+
...(result.public[fragment.scopeId] ?? {}),
|
|
195
|
+
...publicSection,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (Object.keys(privateSection).length) {
|
|
200
|
+
result[fragment.scopeId] = {
|
|
201
|
+
...(result[fragment.scopeId] ?? {}),
|
|
202
|
+
...privateSection,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
package/src/resolve.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createRuntimeConfigKey, type RuntimeConfigKeyOptions } from './key';
|
|
2
|
+
import type { ConfigVisibility, RuntimeConfigSection } from './types';
|
|
3
|
+
|
|
4
|
+
export type ResolveRuntimeConfigValueOptions<T = unknown> = RuntimeConfigKeyOptions & {
|
|
5
|
+
contextId?: string;
|
|
6
|
+
contextOutputKey?: string;
|
|
7
|
+
defaultValue?: T;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ResolveRuntimeConfigValueFromRuntimeConfigOptions<T = unknown> =
|
|
11
|
+
ResolveRuntimeConfigValueOptions<T> & {
|
|
12
|
+
visibility?: ConfigVisibility;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function resolveRuntimeConfigValue<T = unknown>(
|
|
16
|
+
scopeConfig: RuntimeConfigSection | undefined,
|
|
17
|
+
scopeId: string,
|
|
18
|
+
key: string,
|
|
19
|
+
options: ResolveRuntimeConfigValueOptions<T> = {},
|
|
20
|
+
): T | undefined {
|
|
21
|
+
const configKey = createRuntimeConfigKey(scopeId, key, options);
|
|
22
|
+
const contextOutputKey = options.contextOutputKey ?? '__contexts';
|
|
23
|
+
|
|
24
|
+
if (options.contextId) {
|
|
25
|
+
const contexts = scopeConfig?.[contextOutputKey] as
|
|
26
|
+
| Record<string, RuntimeConfigSection>
|
|
27
|
+
| undefined;
|
|
28
|
+
const contextValue = contexts?.[options.contextId]?.[configKey] as T | undefined;
|
|
29
|
+
if (contextValue !== undefined) {
|
|
30
|
+
return contextValue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const globalValue = scopeConfig?.[configKey] as T | undefined;
|
|
35
|
+
if (globalValue !== undefined) {
|
|
36
|
+
return globalValue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return options.defaultValue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveRuntimeConfigValueFromRuntimeConfig<T = unknown>(
|
|
43
|
+
runtimeConfig: Partial<Record<ConfigVisibility, RuntimeConfigSection | undefined>>,
|
|
44
|
+
scopeId: string,
|
|
45
|
+
key: string,
|
|
46
|
+
options: ResolveRuntimeConfigValueFromRuntimeConfigOptions<T> = {},
|
|
47
|
+
): T | undefined {
|
|
48
|
+
const { visibility = 'public', ...resolveOptions } = options;
|
|
49
|
+
|
|
50
|
+
return resolveRuntimeConfigValue(runtimeConfig[visibility], scopeId, key, resolveOptions);
|
|
51
|
+
}
|
package/src/scope.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { stripRuntimeConfigScopePrefix, type RuntimeConfigKeyOptions } from './key';
|
|
2
|
+
import { resolveRuntimeConfigValue } from './resolve';
|
|
3
|
+
import type { ConfigVisibility, RuntimeConfigContext, RuntimeConfigSection } from './types';
|
|
4
|
+
|
|
5
|
+
export type GetRuntimeConfigScopeOptions = RuntimeConfigKeyOptions & {
|
|
6
|
+
contextId?: string;
|
|
7
|
+
contextOutputKey?: string;
|
|
8
|
+
keys?: string[];
|
|
9
|
+
visibility?: ConfigVisibility;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type GetRuntimeConfigFragmentOptions = RuntimeConfigKeyOptions & {
|
|
13
|
+
contextId?: string;
|
|
14
|
+
contextOutputKey?: string;
|
|
15
|
+
keys?: Partial<Record<ConfigVisibility, string[]>>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function assignDefined(target: RuntimeConfigSection, key: string, value: unknown): void {
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
target[key] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectRuntimeConfigScopeValues(input: {
|
|
25
|
+
scopeConfig: RuntimeConfigSection | undefined;
|
|
26
|
+
scopeId: string;
|
|
27
|
+
options: RuntimeConfigKeyOptions;
|
|
28
|
+
}): RuntimeConfigSection {
|
|
29
|
+
const values: RuntimeConfigSection = {};
|
|
30
|
+
|
|
31
|
+
for (const [configKey, value] of Object.entries(input.scopeConfig ?? {})) {
|
|
32
|
+
if (configKey.startsWith('__')) continue;
|
|
33
|
+
|
|
34
|
+
const key = stripRuntimeConfigScopePrefix(input.scopeId, configKey, input.options);
|
|
35
|
+
if (key) assignDefined(values, key, value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return values;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getRuntimeConfigScope<T extends RuntimeConfigSection = RuntimeConfigSection>(
|
|
42
|
+
runtimeConfig: Partial<Record<ConfigVisibility, RuntimeConfigSection | undefined>>,
|
|
43
|
+
scopeId: string,
|
|
44
|
+
options: GetRuntimeConfigScopeOptions = {},
|
|
45
|
+
): T {
|
|
46
|
+
const {
|
|
47
|
+
contextId,
|
|
48
|
+
contextOutputKey = '__contexts',
|
|
49
|
+
keys,
|
|
50
|
+
visibility = 'public',
|
|
51
|
+
...keyOptions
|
|
52
|
+
} = options;
|
|
53
|
+
const scopeConfig = runtimeConfig[visibility];
|
|
54
|
+
|
|
55
|
+
if (keys) {
|
|
56
|
+
return Object.fromEntries(
|
|
57
|
+
keys
|
|
58
|
+
.map((key) => {
|
|
59
|
+
const value = resolveRuntimeConfigValue(scopeConfig, scopeId, key, {
|
|
60
|
+
...keyOptions,
|
|
61
|
+
...(contextId ? { contextId } : {}),
|
|
62
|
+
contextOutputKey,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return [key, value] as const;
|
|
66
|
+
})
|
|
67
|
+
.filter(([, value]) => value !== undefined),
|
|
68
|
+
) as T;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const values = collectRuntimeConfigScopeValues({
|
|
72
|
+
scopeConfig,
|
|
73
|
+
scopeId,
|
|
74
|
+
options: keyOptions,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!contextId) return values as T;
|
|
78
|
+
|
|
79
|
+
const contexts = scopeConfig?.[contextOutputKey] as
|
|
80
|
+
| Record<string, RuntimeConfigSection>
|
|
81
|
+
| undefined;
|
|
82
|
+
const contextValues = collectRuntimeConfigScopeValues({
|
|
83
|
+
scopeConfig: contexts?.[contextId],
|
|
84
|
+
scopeId,
|
|
85
|
+
options: keyOptions,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
...values,
|
|
90
|
+
...contextValues,
|
|
91
|
+
} as T;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getPublicRuntimeConfigScope<T extends RuntimeConfigSection = RuntimeConfigSection>(
|
|
95
|
+
runtimeConfig: Partial<Record<ConfigVisibility, RuntimeConfigSection | undefined>>,
|
|
96
|
+
scopeId: string,
|
|
97
|
+
options: Omit<GetRuntimeConfigScopeOptions, 'visibility'> = {},
|
|
98
|
+
): T {
|
|
99
|
+
return getRuntimeConfigScope<T>(runtimeConfig, scopeId, {
|
|
100
|
+
...options,
|
|
101
|
+
visibility: 'public',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getPrivateRuntimeConfigScope<T extends RuntimeConfigSection = RuntimeConfigSection>(
|
|
106
|
+
runtimeConfig: Partial<Record<ConfigVisibility, RuntimeConfigSection | undefined>>,
|
|
107
|
+
scopeId: string,
|
|
108
|
+
options: Omit<GetRuntimeConfigScopeOptions, 'visibility'> = {},
|
|
109
|
+
): T {
|
|
110
|
+
return getRuntimeConfigScope<T>(runtimeConfig, scopeId, {
|
|
111
|
+
...options,
|
|
112
|
+
visibility: 'private',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getRuntimeConfigFragment(
|
|
117
|
+
runtimeConfig: Partial<Record<ConfigVisibility, RuntimeConfigSection | undefined>>,
|
|
118
|
+
scopeId: string,
|
|
119
|
+
options: GetRuntimeConfigFragmentOptions = {},
|
|
120
|
+
): RuntimeConfigContext {
|
|
121
|
+
const { keys, ...scopeOptions } = options;
|
|
122
|
+
const publicOptions = keys?.public ? { ...scopeOptions, keys: keys.public } : scopeOptions;
|
|
123
|
+
const privateOptions = keys?.private ? { ...scopeOptions, keys: keys.private } : scopeOptions;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
public: getPublicRuntimeConfigScope(runtimeConfig, scopeId, publicOptions),
|
|
127
|
+
private: getPrivateRuntimeConfigScope(runtimeConfig, scopeId, privateOptions),
|
|
128
|
+
};
|
|
129
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ConfigVisibility = 'public' | 'private';
|
|
2
|
+
|
|
3
|
+
export type RuntimeConfigSection = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export type RuntimeConfigContext = {
|
|
6
|
+
public?: RuntimeConfigSection;
|
|
7
|
+
private?: RuntimeConfigSection;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type RuntimeConfigFragment = RuntimeConfigContext & {
|
|
11
|
+
contexts?: Record<string, RuntimeConfigContext>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type RuntimeConfigFragmentInput = RuntimeConfigFragment & Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
export type NamedRuntimeConfigFragment = {
|
|
17
|
+
scopeId: string;
|
|
18
|
+
config: RuntimeConfigFragmentInput;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type RuntimeConfigFragmentMap = Map<string, RuntimeConfigFragment>;
|
|
22
|
+
|
|
23
|
+
export type SectionedRuntimeConfig = {
|
|
24
|
+
public: RuntimeConfigSection;
|
|
25
|
+
private: RuntimeConfigSection;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type RuntimeConfigNamespaceProjection = RuntimeConfigSection & {
|
|
29
|
+
public: RuntimeConfigSection;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type RuntimeEnvVars = Record<string, unknown>;
|
|
33
|
+
|
|
34
|
+
export const runtimeConfigVisibilities: ConfigVisibility[] = ['public', 'private'];
|