@shipfox/client-config 0.2.0
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-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/dist/config-error-screen.d.ts +17 -0
- package/dist/config-error-screen.d.ts.map +1 -0
- package/dist/config-error-screen.js +109 -0
- package/dist/config-error-screen.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/load-config.d.ts +55 -0
- package/dist/load-config.d.ts.map +1 -0
- package/dist/load-config.js +59 -0
- package/dist/load-config.js.map +1 -0
- package/dist/runtime-config.d.ts +17 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +22 -0
- package/dist/runtime-config.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +66 -0
- package/src/config-error-screen.test.tsx +31 -0
- package/src/config-error-screen.tsx +81 -0
- package/src/index.ts +3 -0
- package/src/load-config.test.ts +86 -0
- package/src/load-config.ts +93 -0
- package/src/runtime-config.ts +38 -0
- package/test/setup.ts +3 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +31 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ shipfox-tsc-emit
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ shipfox-tsc-check
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @shipfox/client-config
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3d064b8: Publishes the client runtime closure with shell, feature, route, Vite, and testing contracts.
|
|
8
|
+
|
|
9
|
+
## 0.0.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [c18d624]
|
|
14
|
+
- @shipfox/react-ui@0.3.1
|
|
15
|
+
|
|
16
|
+
## 0.0.1
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [43d7996]
|
|
21
|
+
- Updated dependencies [14e0bea]
|
|
22
|
+
- Updated dependencies [9018f0b]
|
|
23
|
+
- Updated dependencies [7fdfd72]
|
|
24
|
+
- Updated dependencies [2a3193f]
|
|
25
|
+
- Updated dependencies [f104ff2]
|
|
26
|
+
- Updated dependencies [7341569]
|
|
27
|
+
- Updated dependencies [e4c6abf]
|
|
28
|
+
- Updated dependencies [a35c2dc]
|
|
29
|
+
- Updated dependencies [58f7aef]
|
|
30
|
+
- Updated dependencies [5264a22]
|
|
31
|
+
- Updated dependencies [9674879]
|
|
32
|
+
- Updated dependencies [225c9a5]
|
|
33
|
+
- Updated dependencies [24f131b]
|
|
34
|
+
- Updated dependencies [bb2a7bc]
|
|
35
|
+
- Updated dependencies [5eb06d0]
|
|
36
|
+
- Updated dependencies [4e13e5f]
|
|
37
|
+
- Updated dependencies [e92150d]
|
|
38
|
+
- Updated dependencies [8037501]
|
|
39
|
+
- Updated dependencies [0fb6018]
|
|
40
|
+
- Updated dependencies [c27a1ed]
|
|
41
|
+
- Updated dependencies [b8e49ff]
|
|
42
|
+
- Updated dependencies [8037501]
|
|
43
|
+
- Updated dependencies [6c0da64]
|
|
44
|
+
- Updated dependencies [07f8ff8]
|
|
45
|
+
- Updated dependencies [e457582]
|
|
46
|
+
- Updated dependencies [8b5c905]
|
|
47
|
+
- Updated dependencies [f849131]
|
|
48
|
+
- Updated dependencies [94bdcc5]
|
|
49
|
+
- Updated dependencies [a34c8ea]
|
|
50
|
+
- Updated dependencies [8ac4bf4]
|
|
51
|
+
- Updated dependencies [3a0be6b]
|
|
52
|
+
- Updated dependencies [d42baf4]
|
|
53
|
+
- Updated dependencies [8037501]
|
|
54
|
+
- Updated dependencies [54bb8a3]
|
|
55
|
+
- Updated dependencies [f711e18]
|
|
56
|
+
- @shipfox/react-ui@0.3.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shipfox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConfigKeyError } from './load-config.js';
|
|
2
|
+
export interface ConfigErrorScreenProps {
|
|
3
|
+
errors: ConfigKeyError[];
|
|
4
|
+
/** Link to the self-hosting configuration guide. */
|
|
5
|
+
docsUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Full-screen configuration diagnostic shown instead of the app when required
|
|
9
|
+
* config is missing or invalid. It lists every problem at once with the exact
|
|
10
|
+
* environment variable to set, so a self-hoster fixes the deployment in one pass
|
|
11
|
+
* rather than discovering errors one failed request at a time.
|
|
12
|
+
*
|
|
13
|
+
* Rendered outside the app's auth/router tree but inside `ThemeProvider` (see
|
|
14
|
+
* main.tsx), so it uses the design system directly.
|
|
15
|
+
*/
|
|
16
|
+
export declare function ConfigErrorScreen({ errors, docsUrl }: ConfigErrorScreenProps): import("react").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=config-error-screen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-error-screen.d.ts","sourceRoot":"","sources":["../src/config-error-screen.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAErD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,EAAC,MAAM,EAAE,OAAO,EAAC,EAAE,sBAAsB,+BA2D1E"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ButtonLink } from '@shipfox/react-ui/button';
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@shipfox/react-ui/card';
|
|
4
|
+
import { Icon } from '@shipfox/react-ui/icon';
|
|
5
|
+
import { Code, Text } from '@shipfox/react-ui/typography';
|
|
6
|
+
/**
|
|
7
|
+
* Full-screen configuration diagnostic shown instead of the app when required
|
|
8
|
+
* config is missing or invalid. It lists every problem at once with the exact
|
|
9
|
+
* environment variable to set, so a self-hoster fixes the deployment in one pass
|
|
10
|
+
* rather than discovering errors one failed request at a time.
|
|
11
|
+
*
|
|
12
|
+
* Rendered outside the app's auth/router tree but inside `ThemeProvider` (see
|
|
13
|
+
* main.tsx), so it uses the design system directly.
|
|
14
|
+
*/ export function ConfigErrorScreen({ errors, docsUrl }) {
|
|
15
|
+
return /*#__PURE__*/ _jsx("main", {
|
|
16
|
+
className: "flex min-h-screen items-center justify-center bg-background-subtle-base px-24 py-32",
|
|
17
|
+
children: /*#__PURE__*/ _jsxs(Card, {
|
|
18
|
+
className: "w-full max-w-[512px]",
|
|
19
|
+
children: [
|
|
20
|
+
/*#__PURE__*/ _jsxs(CardHeader, {
|
|
21
|
+
children: [
|
|
22
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
23
|
+
className: "flex items-center gap-8",
|
|
24
|
+
children: [
|
|
25
|
+
/*#__PURE__*/ _jsx(Icon, {
|
|
26
|
+
name: "errorWarningLine",
|
|
27
|
+
className: "size-20 text-tag-error-icon"
|
|
28
|
+
}),
|
|
29
|
+
/*#__PURE__*/ _jsx(CardTitle, {
|
|
30
|
+
variant: "h2",
|
|
31
|
+
children: "Configuration error"
|
|
32
|
+
})
|
|
33
|
+
]
|
|
34
|
+
}),
|
|
35
|
+
/*#__PURE__*/ _jsx(CardDescription, {
|
|
36
|
+
children: "The Shipfox client could not start because its configuration is missing or invalid. Set the environment variables below and restart the container."
|
|
37
|
+
})
|
|
38
|
+
]
|
|
39
|
+
}),
|
|
40
|
+
/*#__PURE__*/ _jsx(CardContent, {
|
|
41
|
+
className: "flex flex-col",
|
|
42
|
+
children: errors.map((error)=>/*#__PURE__*/ _jsxs("div", {
|
|
43
|
+
className: "flex flex-col gap-4 border-t border-border-neutral-base py-12 first:border-t-0 first:pt-0",
|
|
44
|
+
children: [
|
|
45
|
+
/*#__PURE__*/ _jsx(Code, {
|
|
46
|
+
variant: "label",
|
|
47
|
+
bold: true,
|
|
48
|
+
className: "text-foreground-neutral-base",
|
|
49
|
+
children: error.key
|
|
50
|
+
}),
|
|
51
|
+
error.description ? /*#__PURE__*/ _jsx(Text, {
|
|
52
|
+
size: "sm",
|
|
53
|
+
className: "text-foreground-neutral-muted",
|
|
54
|
+
children: error.description
|
|
55
|
+
}) : null,
|
|
56
|
+
/*#__PURE__*/ _jsx(Text, {
|
|
57
|
+
size: "sm",
|
|
58
|
+
className: "text-tag-error-text",
|
|
59
|
+
children: error.message
|
|
60
|
+
}),
|
|
61
|
+
/*#__PURE__*/ _jsxs(Text, {
|
|
62
|
+
size: "xs",
|
|
63
|
+
className: "text-foreground-neutral-muted",
|
|
64
|
+
children: [
|
|
65
|
+
"Set",
|
|
66
|
+
' ',
|
|
67
|
+
/*#__PURE__*/ _jsx(Code, {
|
|
68
|
+
as: "code",
|
|
69
|
+
variant: "label",
|
|
70
|
+
className: "text-foreground-neutral-subtle",
|
|
71
|
+
children: error.envVars[0]
|
|
72
|
+
}),
|
|
73
|
+
' ',
|
|
74
|
+
"(self-hosting) or",
|
|
75
|
+
' ',
|
|
76
|
+
/*#__PURE__*/ _jsx(Code, {
|
|
77
|
+
as: "code",
|
|
78
|
+
variant: "label",
|
|
79
|
+
className: "text-foreground-neutral-subtle",
|
|
80
|
+
children: error.envVars[1]
|
|
81
|
+
}),
|
|
82
|
+
' ',
|
|
83
|
+
"(build time)."
|
|
84
|
+
]
|
|
85
|
+
})
|
|
86
|
+
]
|
|
87
|
+
}, error.key))
|
|
88
|
+
}),
|
|
89
|
+
docsUrl ? /*#__PURE__*/ _jsxs(Text, {
|
|
90
|
+
size: "sm",
|
|
91
|
+
className: "text-foreground-neutral-muted",
|
|
92
|
+
children: [
|
|
93
|
+
"See the",
|
|
94
|
+
' ',
|
|
95
|
+
/*#__PURE__*/ _jsx(ButtonLink, {
|
|
96
|
+
href: docsUrl,
|
|
97
|
+
variant: "interactive",
|
|
98
|
+
underline: true,
|
|
99
|
+
children: "self-hosting configuration guide"
|
|
100
|
+
}),
|
|
101
|
+
"."
|
|
102
|
+
]
|
|
103
|
+
}) : null
|
|
104
|
+
]
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=config-error-screen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config-error-screen.tsx"],"sourcesContent":["import {ButtonLink} from '@shipfox/react-ui/button';\nimport {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@shipfox/react-ui/card';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Code, Text} from '@shipfox/react-ui/typography';\nimport type {ConfigKeyError} from './load-config.js';\n\nexport interface ConfigErrorScreenProps {\n errors: ConfigKeyError[];\n /** Link to the self-hosting configuration guide. */\n docsUrl?: string;\n}\n\n/**\n * Full-screen configuration diagnostic shown instead of the app when required\n * config is missing or invalid. It lists every problem at once with the exact\n * environment variable to set, so a self-hoster fixes the deployment in one pass\n * rather than discovering errors one failed request at a time.\n *\n * Rendered outside the app's auth/router tree but inside `ThemeProvider` (see\n * main.tsx), so it uses the design system directly.\n */\nexport function ConfigErrorScreen({errors, docsUrl}: ConfigErrorScreenProps) {\n return (\n <main className=\"flex min-h-screen items-center justify-center bg-background-subtle-base px-24 py-32\">\n <Card className=\"w-full max-w-[512px]\">\n <CardHeader>\n <div className=\"flex items-center gap-8\">\n <Icon name=\"errorWarningLine\" className=\"size-20 text-tag-error-icon\" />\n <CardTitle variant=\"h2\">Configuration error</CardTitle>\n </div>\n <CardDescription>\n The Shipfox client could not start because its configuration is missing or invalid. Set\n the environment variables below and restart the container.\n </CardDescription>\n </CardHeader>\n\n <CardContent className=\"flex flex-col\">\n {errors.map((error) => (\n <div\n key={error.key}\n className=\"flex flex-col gap-4 border-t border-border-neutral-base py-12 first:border-t-0 first:pt-0\"\n >\n <Code variant=\"label\" bold className=\"text-foreground-neutral-base\">\n {error.key}\n </Code>\n {error.description ? (\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n {error.description}\n </Text>\n ) : null}\n <Text size=\"sm\" className=\"text-tag-error-text\">\n {error.message}\n </Text>\n <Text size=\"xs\" className=\"text-foreground-neutral-muted\">\n Set{' '}\n <Code as=\"code\" variant=\"label\" className=\"text-foreground-neutral-subtle\">\n {error.envVars[0]}\n </Code>{' '}\n (self-hosting) or{' '}\n <Code as=\"code\" variant=\"label\" className=\"text-foreground-neutral-subtle\">\n {error.envVars[1]}\n </Code>{' '}\n (build time).\n </Text>\n </div>\n ))}\n </CardContent>\n\n {docsUrl ? (\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n See the{' '}\n <ButtonLink href={docsUrl} variant=\"interactive\" underline>\n self-hosting configuration guide\n </ButtonLink>\n .\n </Text>\n ) : null}\n </Card>\n </main>\n );\n}\n"],"names":["ButtonLink","Card","CardContent","CardDescription","CardHeader","CardTitle","Icon","Code","Text","ConfigErrorScreen","errors","docsUrl","main","className","div","name","variant","map","error","bold","key","description","size","message","as","envVars","href","underline"],"mappings":";AAAA,SAAQA,UAAU,QAAO,2BAA2B;AACpD,SAAQC,IAAI,EAAEC,WAAW,EAAEC,eAAe,EAAEC,UAAU,EAAEC,SAAS,QAAO,yBAAyB;AACjG,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,IAAI,EAAEC,IAAI,QAAO,+BAA+B;AASxD;;;;;;;;CAQC,GACD,OAAO,SAASC,kBAAkB,EAACC,MAAM,EAAEC,OAAO,EAAyB;IACzE,qBACE,KAACC;QAAKC,WAAU;kBACd,cAAA,MAACZ;YAAKY,WAAU;;8BACd,MAACT;;sCACC,MAACU;4BAAID,WAAU;;8CACb,KAACP;oCAAKS,MAAK;oCAAmBF,WAAU;;8CACxC,KAACR;oCAAUW,SAAQ;8CAAK;;;;sCAE1B,KAACb;sCAAgB;;;;8BAMnB,KAACD;oBAAYW,WAAU;8BACpBH,OAAOO,GAAG,CAAC,CAACC,sBACX,MAACJ;4BAECD,WAAU;;8CAEV,KAACN;oCAAKS,SAAQ;oCAAQG,IAAI;oCAACN,WAAU;8CAClCK,MAAME,GAAG;;gCAEXF,MAAMG,WAAW,iBAChB,KAACb;oCAAKc,MAAK;oCAAKT,WAAU;8CACvBK,MAAMG,WAAW;qCAElB;8CACJ,KAACb;oCAAKc,MAAK;oCAAKT,WAAU;8CACvBK,MAAMK,OAAO;;8CAEhB,MAACf;oCAAKc,MAAK;oCAAKT,WAAU;;wCAAgC;wCACpD;sDACJ,KAACN;4CAAKiB,IAAG;4CAAOR,SAAQ;4CAAQH,WAAU;sDACvCK,MAAMO,OAAO,CAAC,EAAE;;wCACX;wCAAI;wCACM;sDAClB,KAAClB;4CAAKiB,IAAG;4CAAOR,SAAQ;4CAAQH,WAAU;sDACvCK,MAAMO,OAAO,CAAC,EAAE;;wCACX;wCAAI;;;;2BAtBTP,MAAME,GAAG;;gBA6BnBT,wBACC,MAACH;oBAAKc,MAAK;oBAAKT,WAAU;;wBAAgC;wBAChD;sCACR,KAACb;4BAAW0B,MAAMf;4BAASK,SAAQ;4BAAcW,SAAS;sCAAC;;wBAE9C;;qBAGb;;;;AAIZ"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './config-error-screen.js';\nexport * from './load-config.js';\nexport * from './runtime-config.js';\n"],"names":[],"mappings":"AAAA,cAAc,2BAA2B;AACzC,cAAc,mBAAmB;AACjC,cAAc,sBAAsB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A config schema fragment: the Zod shape a feature module contributes. The app
|
|
4
|
+
* composes fragments from every module into one shape, then validates it once.
|
|
5
|
+
*/
|
|
6
|
+
export type ConfigShape = z.ZodRawShape;
|
|
7
|
+
/**
|
|
8
|
+
* The two places a config value can come from. The same image serves any
|
|
9
|
+
* deployment because the app reads both and lets `runtime` win:
|
|
10
|
+
*
|
|
11
|
+
* - `runtime` is `window.__SHIPFOX_CONFIG__`, written per deployment (the
|
|
12
|
+
* self-host Docker entrypoint, or a Vercel edge function). Keys are the
|
|
13
|
+
* SCREAMING_SNAKE form of each config key, matching the env var suffix
|
|
14
|
+
* (`apiUrl` -> `API_URL`).
|
|
15
|
+
* - `build` is `import.meta.env`, baked by Vite. Keys are `VITE_`-prefixed
|
|
16
|
+
* (`VITE_API_URL`). Used by the Vercel SaaS build and in local dev.
|
|
17
|
+
*/
|
|
18
|
+
export interface ConfigSources {
|
|
19
|
+
runtime?: Record<string, unknown> | undefined;
|
|
20
|
+
build?: Record<string, unknown> | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** One missing or invalid config key, shaped for reporting to a self-hoster. */
|
|
23
|
+
export interface ConfigKeyError {
|
|
24
|
+
key: string;
|
|
25
|
+
/** The env vars that set this key: `SHIPFOX_PUBLIC_*` (self-host), `VITE_*` (Vercel/dev). */
|
|
26
|
+
envVars: string[];
|
|
27
|
+
description: string | undefined;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
export type ConfigResult<TConfig> = {
|
|
31
|
+
ok: true;
|
|
32
|
+
config: TConfig;
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
errors: ConfigKeyError[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Maps a camelCase config key to the SCREAMING_SNAKE suffix shared by its env
|
|
39
|
+
* vars and its runtime-global key: `apiUrl` -> `API_URL`,
|
|
40
|
+
* `datadogClientToken` -> `DATADOG_CLIENT_TOKEN`. Deriving every name from one
|
|
41
|
+
* key is what keeps adding a config key to a single Zod-fragment edit.
|
|
42
|
+
*/
|
|
43
|
+
export declare function envNameFor(key: string): string;
|
|
44
|
+
/** The `SHIPFOX_PUBLIC_*` then `VITE_*` env var names that feed a config key. */
|
|
45
|
+
export declare function envVarsFor(key: string): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Resolves and validates the config from both sources in a single pass.
|
|
48
|
+
*
|
|
49
|
+
* For each schema key, the runtime value wins over the build value; a value
|
|
50
|
+
* that is absent from both is left to the schema (its default applies, or it is
|
|
51
|
+
* reported as missing). Validation is aggregated, never fail-fast, so a
|
|
52
|
+
* misconfigured deployment learns about every problem at once.
|
|
53
|
+
*/
|
|
54
|
+
export declare function loadConfig<TShape extends ConfigShape>(shape: TShape, sources: ConfigSources): ConfigResult<z.infer<z.ZodObject<TShape>>>;
|
|
55
|
+
//# sourceMappingURL=load-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,WAAW,CAAC;AAExC;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CAC7C;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,6FAA6F;IAC7F,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,CAAC,OAAO,IAC5B;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,GAC3B;IAAC,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,cAAc,EAAE,CAAA;CAAC,CAAC;AAE1C;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAGhD;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAS,WAAW,EACnD,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,GACrB,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAY5C"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Maps a camelCase config key to the SCREAMING_SNAKE suffix shared by its env
|
|
4
|
+
* vars and its runtime-global key: `apiUrl` -> `API_URL`,
|
|
5
|
+
* `datadogClientToken` -> `DATADOG_CLIENT_TOKEN`. Deriving every name from one
|
|
6
|
+
* key is what keeps adding a config key to a single Zod-fragment edit.
|
|
7
|
+
*/ export function envNameFor(key) {
|
|
8
|
+
return key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
9
|
+
}
|
|
10
|
+
/** The `SHIPFOX_PUBLIC_*` then `VITE_*` env var names that feed a config key. */ export function envVarsFor(key) {
|
|
11
|
+
const suffix = envNameFor(key);
|
|
12
|
+
return [
|
|
13
|
+
`SHIPFOX_PUBLIC_${suffix}`,
|
|
14
|
+
`VITE_${suffix}`
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolves and validates the config from both sources in a single pass.
|
|
19
|
+
*
|
|
20
|
+
* For each schema key, the runtime value wins over the build value; a value
|
|
21
|
+
* that is absent from both is left to the schema (its default applies, or it is
|
|
22
|
+
* reported as missing). Validation is aggregated, never fail-fast, so a
|
|
23
|
+
* misconfigured deployment learns about every problem at once.
|
|
24
|
+
*/ export function loadConfig(shape, sources) {
|
|
25
|
+
const input = {};
|
|
26
|
+
for (const key of Object.keys(shape)){
|
|
27
|
+
const suffix = envNameFor(key);
|
|
28
|
+
const value = sources.runtime?.[suffix] ?? sources.build?.[`VITE_${suffix}`];
|
|
29
|
+
if (value !== undefined) input[key] = value;
|
|
30
|
+
}
|
|
31
|
+
const result = z.object(shape).safeParse(input);
|
|
32
|
+
if (result.success) return {
|
|
33
|
+
ok: true,
|
|
34
|
+
config: result.data
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
errors: toKeyErrors(result.error, shape)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function toKeyErrors(error, shape) {
|
|
42
|
+
const byKey = new Map();
|
|
43
|
+
for (const issue of error.issues){
|
|
44
|
+
const key = String(issue.path[0] ?? '');
|
|
45
|
+
if (byKey.has(key)) continue;
|
|
46
|
+
const field = shape[key];
|
|
47
|
+
byKey.set(key, {
|
|
48
|
+
key,
|
|
49
|
+
envVars: envVarsFor(key),
|
|
50
|
+
description: field?.description,
|
|
51
|
+
message: issue.message
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
...byKey.values()
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=load-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/load-config.ts"],"sourcesContent":["import {z} from 'zod';\n\n/**\n * A config schema fragment: the Zod shape a feature module contributes. The app\n * composes fragments from every module into one shape, then validates it once.\n */\nexport type ConfigShape = z.ZodRawShape;\n\n/**\n * The two places a config value can come from. The same image serves any\n * deployment because the app reads both and lets `runtime` win:\n *\n * - `runtime` is `window.__SHIPFOX_CONFIG__`, written per deployment (the\n * self-host Docker entrypoint, or a Vercel edge function). Keys are the\n * SCREAMING_SNAKE form of each config key, matching the env var suffix\n * (`apiUrl` -> `API_URL`).\n * - `build` is `import.meta.env`, baked by Vite. Keys are `VITE_`-prefixed\n * (`VITE_API_URL`). Used by the Vercel SaaS build and in local dev.\n */\nexport interface ConfigSources {\n runtime?: Record<string, unknown> | undefined;\n build?: Record<string, unknown> | undefined;\n}\n\n/** One missing or invalid config key, shaped for reporting to a self-hoster. */\nexport interface ConfigKeyError {\n key: string;\n /** The env vars that set this key: `SHIPFOX_PUBLIC_*` (self-host), `VITE_*` (Vercel/dev). */\n envVars: string[];\n description: string | undefined;\n message: string;\n}\n\nexport type ConfigResult<TConfig> =\n | {ok: true; config: TConfig}\n | {ok: false; errors: ConfigKeyError[]};\n\n/**\n * Maps a camelCase config key to the SCREAMING_SNAKE suffix shared by its env\n * vars and its runtime-global key: `apiUrl` -> `API_URL`,\n * `datadogClientToken` -> `DATADOG_CLIENT_TOKEN`. Deriving every name from one\n * key is what keeps adding a config key to a single Zod-fragment edit.\n */\nexport function envNameFor(key: string): string {\n return key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();\n}\n\n/** The `SHIPFOX_PUBLIC_*` then `VITE_*` env var names that feed a config key. */\nexport function envVarsFor(key: string): string[] {\n const suffix = envNameFor(key);\n return [`SHIPFOX_PUBLIC_${suffix}`, `VITE_${suffix}`];\n}\n\n/**\n * Resolves and validates the config from both sources in a single pass.\n *\n * For each schema key, the runtime value wins over the build value; a value\n * that is absent from both is left to the schema (its default applies, or it is\n * reported as missing). Validation is aggregated, never fail-fast, so a\n * misconfigured deployment learns about every problem at once.\n */\nexport function loadConfig<TShape extends ConfigShape>(\n shape: TShape,\n sources: ConfigSources,\n): ConfigResult<z.infer<z.ZodObject<TShape>>> {\n const input: Record<string, unknown> = {};\n for (const key of Object.keys(shape)) {\n const suffix = envNameFor(key);\n const value = sources.runtime?.[suffix] ?? sources.build?.[`VITE_${suffix}`];\n if (value !== undefined) input[key] = value;\n }\n\n const result = z.object(shape).safeParse(input);\n if (result.success) return {ok: true, config: result.data};\n\n return {ok: false, errors: toKeyErrors(result.error, shape)};\n}\n\nfunction toKeyErrors(error: z.ZodError, shape: ConfigShape): ConfigKeyError[] {\n const byKey = new Map<string, ConfigKeyError>();\n for (const issue of error.issues) {\n const key = String(issue.path[0] ?? '');\n if (byKey.has(key)) continue;\n const field = shape[key] as {description?: string} | undefined;\n byKey.set(key, {\n key,\n envVars: envVarsFor(key),\n description: field?.description,\n message: issue.message,\n });\n }\n return [...byKey.values()];\n}\n"],"names":["z","envNameFor","key","replace","toUpperCase","envVarsFor","suffix","loadConfig","shape","sources","input","Object","keys","value","runtime","build","undefined","result","object","safeParse","success","ok","config","data","errors","toKeyErrors","error","byKey","Map","issue","issues","String","path","has","field","set","envVars","description","message","values"],"mappings":"AAAA,SAAQA,CAAC,QAAO,MAAM;AAqCtB;;;;;CAKC,GACD,OAAO,SAASC,WAAWC,GAAW;IACpC,OAAOA,IAAIC,OAAO,CAAC,sBAAsB,SAASC,WAAW;AAC/D;AAEA,+EAA+E,GAC/E,OAAO,SAASC,WAAWH,GAAW;IACpC,MAAMI,SAASL,WAAWC;IAC1B,OAAO;QAAC,CAAC,eAAe,EAAEI,QAAQ;QAAE,CAAC,KAAK,EAAEA,QAAQ;KAAC;AACvD;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,WACdC,KAAa,EACbC,OAAsB;IAEtB,MAAMC,QAAiC,CAAC;IACxC,KAAK,MAAMR,OAAOS,OAAOC,IAAI,CAACJ,OAAQ;QACpC,MAAMF,SAASL,WAAWC;QAC1B,MAAMW,QAAQJ,QAAQK,OAAO,EAAE,CAACR,OAAO,IAAIG,QAAQM,KAAK,EAAE,CAAC,CAAC,KAAK,EAAET,QAAQ,CAAC;QAC5E,IAAIO,UAAUG,WAAWN,KAAK,CAACR,IAAI,GAAGW;IACxC;IAEA,MAAMI,SAASjB,EAAEkB,MAAM,CAACV,OAAOW,SAAS,CAACT;IACzC,IAAIO,OAAOG,OAAO,EAAE,OAAO;QAACC,IAAI;QAAMC,QAAQL,OAAOM,IAAI;IAAA;IAEzD,OAAO;QAACF,IAAI;QAAOG,QAAQC,YAAYR,OAAOS,KAAK,EAAElB;IAAM;AAC7D;AAEA,SAASiB,YAAYC,KAAiB,EAAElB,KAAkB;IACxD,MAAMmB,QAAQ,IAAIC;IAClB,KAAK,MAAMC,SAASH,MAAMI,MAAM,CAAE;QAChC,MAAM5B,MAAM6B,OAAOF,MAAMG,IAAI,CAAC,EAAE,IAAI;QACpC,IAAIL,MAAMM,GAAG,CAAC/B,MAAM;QACpB,MAAMgC,QAAQ1B,KAAK,CAACN,IAAI;QACxByB,MAAMQ,GAAG,CAACjC,KAAK;YACbA;YACAkC,SAAS/B,WAAWH;YACpBmC,aAAaH,OAAOG;YACpBC,SAAST,MAAMS,OAAO;QACxB;IACF;IACA,OAAO;WAAIX,MAAMY,MAAM;KAAG;AAC5B"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
__SHIPFOX_CONFIG__?: Record<string, unknown> | undefined;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
/** Reads `window.__SHIPFOX_CONFIG__`, or undefined when not in a browser. */
|
|
7
|
+
export declare function getWindowRuntimeConfig(): Record<string, unknown> | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Stores the validated config as a frozen, app-wide singleton at boot. The
|
|
10
|
+
* config is immutable for the lifetime of the page, so non-React code (such as
|
|
11
|
+
* the API client wiring) and `useConfig` can both read it without a provider.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setLoadedConfig(config: unknown): void;
|
|
14
|
+
export declare function getLoadedConfig<TConfig>(): TConfig;
|
|
15
|
+
/** React accessor for the validated config. Config is immutable post-boot. */
|
|
16
|
+
export declare function useConfig<TConfig>(): TConfig;
|
|
17
|
+
//# sourceMappingURL=runtime-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-config.d.ts","sourceRoot":"","sources":["../src/runtime-config.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QAId,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC1D;CACF;AAED,6EAA6E;AAC7E,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAE5E;AAID;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAErD;AAED,wBAAgB,eAAe,CAAC,OAAO,KAAK,OAAO,CAOlD;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,OAAO,KAAK,OAAO,CAE5C"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Reads `window.__SHIPFOX_CONFIG__`, or undefined when not in a browser. */ export function getWindowRuntimeConfig() {
|
|
2
|
+
return typeof window === 'undefined' ? undefined : window.__SHIPFOX_CONFIG__;
|
|
3
|
+
}
|
|
4
|
+
let loaded;
|
|
5
|
+
/**
|
|
6
|
+
* Stores the validated config as a frozen, app-wide singleton at boot. The
|
|
7
|
+
* config is immutable for the lifetime of the page, so non-React code (such as
|
|
8
|
+
* the API client wiring) and `useConfig` can both read it without a provider.
|
|
9
|
+
*/ export function setLoadedConfig(config) {
|
|
10
|
+
loaded = Object.freeze(config);
|
|
11
|
+
}
|
|
12
|
+
export function getLoadedConfig() {
|
|
13
|
+
if (loaded === undefined) {
|
|
14
|
+
throw new Error('Runtime config has not been loaded. Call loadConfig + setLoadedConfig at app boot before reading it.');
|
|
15
|
+
}
|
|
16
|
+
return loaded;
|
|
17
|
+
}
|
|
18
|
+
/** React accessor for the validated config. Config is immutable post-boot. */ export function useConfig() {
|
|
19
|
+
return getLoadedConfig();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=runtime-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runtime-config.ts"],"sourcesContent":["declare global {\n interface Window {\n // The runtime config object written before the app bundle loads. The\n // self-host Docker entrypoint (or a Vercel edge function) populates it from\n // environment variables; keys are SCREAMING_SNAKE (see envNameFor).\n __SHIPFOX_CONFIG__?: Record<string, unknown> | undefined;\n }\n}\n\n/** Reads `window.__SHIPFOX_CONFIG__`, or undefined when not in a browser. */\nexport function getWindowRuntimeConfig(): Record<string, unknown> | undefined {\n return typeof window === 'undefined' ? undefined : window.__SHIPFOX_CONFIG__;\n}\n\nlet loaded: unknown;\n\n/**\n * Stores the validated config as a frozen, app-wide singleton at boot. The\n * config is immutable for the lifetime of the page, so non-React code (such as\n * the API client wiring) and `useConfig` can both read it without a provider.\n */\nexport function setLoadedConfig(config: unknown): void {\n loaded = Object.freeze(config);\n}\n\nexport function getLoadedConfig<TConfig>(): TConfig {\n if (loaded === undefined) {\n throw new Error(\n 'Runtime config has not been loaded. Call loadConfig + setLoadedConfig at app boot before reading it.',\n );\n }\n return loaded as TConfig;\n}\n\n/** React accessor for the validated config. Config is immutable post-boot. */\nexport function useConfig<TConfig>(): TConfig {\n return getLoadedConfig<TConfig>();\n}\n"],"names":["getWindowRuntimeConfig","window","undefined","__SHIPFOX_CONFIG__","loaded","setLoadedConfig","config","Object","freeze","getLoadedConfig","Error","useConfig"],"mappings":"AASA,2EAA2E,GAC3E,OAAO,SAASA;IACd,OAAO,OAAOC,WAAW,cAAcC,YAAYD,OAAOE,kBAAkB;AAC9E;AAEA,IAAIC;AAEJ;;;;CAIC,GACD,OAAO,SAASC,gBAAgBC,MAAe;IAC7CF,SAASG,OAAOC,MAAM,CAACF;AACzB;AAEA,OAAO,SAASG;IACd,IAAIL,WAAWF,WAAW;QACxB,MAAM,IAAIQ,MACR;IAEJ;IACA,OAAON;AACT;AAEA,4EAA4E,GAC5E,OAAO,SAASO;IACd,OAAOF;AACT"}
|