@next-box/appsettings 0.0.0-stage → 0.0.4
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 -0
- package/README.md +182 -2
- package/bin/appsettings.mjs +7 -0
- package/dist/cjs/cli/command.cjs +2 -0
- package/dist/cjs/cli/command.cjs.map +1 -0
- package/dist/cjs/cli/handler.cjs +2 -0
- package/dist/cjs/cli/handler.cjs.map +1 -0
- package/dist/cjs/cli.cjs +2 -0
- package/dist/cjs/cli.cjs.map +1 -0
- package/dist/cjs/components/AppSettingsProvider.cjs +3 -0
- package/dist/cjs/components/AppSettingsProvider.cjs.map +1 -0
- package/dist/cjs/components/Setting.cjs +3 -0
- package/dist/cjs/components/Setting.cjs.map +1 -0
- package/dist/cjs/config/AppSettingsImplementation.cjs +2 -0
- package/dist/cjs/config/AppSettingsImplementation.cjs.map +1 -0
- package/dist/cjs/config/ConfigMerger.cjs +2 -0
- package/dist/cjs/config/ConfigMerger.cjs.map +1 -0
- package/dist/cjs/config/ConfigReader.cjs +2 -0
- package/dist/cjs/config/ConfigReader.cjs.map +1 -0
- package/dist/cjs/config/ConfigStore.cjs +2 -0
- package/dist/cjs/config/ConfigStore.cjs.map +1 -0
- package/dist/cjs/config/ConfigValidator.cjs +2 -0
- package/dist/cjs/config/ConfigValidator.cjs.map +1 -0
- package/dist/cjs/config/ScopedSettingsImplementation.cjs +2 -0
- package/dist/cjs/config/ScopedSettingsImplementation.cjs.map +1 -0
- package/dist/cjs/config/createAppSettings.cjs +2 -0
- package/dist/cjs/config/createAppSettings.cjs.map +1 -0
- package/dist/cjs/config/helpers/freezeDeep.cjs +2 -0
- package/dist/cjs/config/helpers/freezeDeep.cjs.map +1 -0
- package/dist/cjs/hooks/useAppSetting.cjs +2 -0
- package/dist/cjs/hooks/useAppSetting.cjs.map +1 -0
- package/dist/cjs/hooks/useAppSettings.cjs +2 -0
- package/dist/cjs/hooks/useAppSettings.cjs.map +1 -0
- package/dist/cjs/hooks/useAppSettingsContext.cjs +2 -0
- package/dist/cjs/hooks/useAppSettingsContext.cjs.map +1 -0
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/server.cjs +2 -0
- package/dist/cjs/server.cjs.map +1 -0
- package/dist/cjs/sources/AppSettingsSource.cjs +2 -0
- package/dist/cjs/sources/AppSettingsSource.cjs.map +1 -0
- package/dist/cjs/sources/EnvironmentSource.cjs +2 -0
- package/dist/cjs/sources/EnvironmentSource.cjs.map +1 -0
- package/dist/cjs/sources/MemorySource.cjs +2 -0
- package/dist/cjs/sources/MemorySource.cjs.map +1 -0
- package/dist/cjs/sources/RemoteSource.cjs +2 -0
- package/dist/cjs/sources/RemoteSource.cjs.map +1 -0
- package/dist/cjs/sources/helpers/loadJson.cjs +2 -0
- package/dist/cjs/sources/helpers/loadJson.cjs.map +1 -0
- package/dist/esm/cli/command.mjs +2 -0
- package/dist/esm/cli/command.mjs.map +1 -0
- package/dist/esm/cli/handler.mjs +2 -0
- package/dist/esm/cli/handler.mjs.map +1 -0
- package/dist/esm/cli.mjs +2 -0
- package/dist/esm/cli.mjs.map +1 -0
- package/dist/esm/components/AppSettingsProvider.mjs +3 -0
- package/dist/esm/components/AppSettingsProvider.mjs.map +1 -0
- package/dist/esm/components/Setting.mjs +3 -0
- package/dist/esm/components/Setting.mjs.map +1 -0
- package/dist/esm/config/AppSettingsImplementation.mjs +2 -0
- package/dist/esm/config/AppSettingsImplementation.mjs.map +1 -0
- package/dist/esm/config/ConfigMerger.mjs +2 -0
- package/dist/esm/config/ConfigMerger.mjs.map +1 -0
- package/dist/esm/config/ConfigReader.mjs +2 -0
- package/dist/esm/config/ConfigReader.mjs.map +1 -0
- package/dist/esm/config/ConfigStore.mjs +2 -0
- package/dist/esm/config/ConfigStore.mjs.map +1 -0
- package/dist/esm/config/ConfigValidator.mjs +2 -0
- package/dist/esm/config/ConfigValidator.mjs.map +1 -0
- package/dist/esm/config/ScopedSettingsImplementation.mjs +2 -0
- package/dist/esm/config/ScopedSettingsImplementation.mjs.map +1 -0
- package/dist/esm/config/createAppSettings.mjs +2 -0
- package/dist/esm/config/createAppSettings.mjs.map +1 -0
- package/dist/esm/config/helpers/freezeDeep.mjs +2 -0
- package/dist/esm/config/helpers/freezeDeep.mjs.map +1 -0
- package/dist/esm/hooks/useAppSetting.mjs +2 -0
- package/dist/esm/hooks/useAppSetting.mjs.map +1 -0
- package/dist/esm/hooks/useAppSettings.mjs +2 -0
- package/dist/esm/hooks/useAppSettings.mjs.map +1 -0
- package/dist/esm/hooks/useAppSettingsContext.mjs +2 -0
- package/dist/esm/hooks/useAppSettingsContext.mjs.map +1 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/server.mjs +2 -0
- package/dist/esm/server.mjs.map +1 -0
- package/dist/esm/sources/AppSettingsSource.mjs +2 -0
- package/dist/esm/sources/AppSettingsSource.mjs.map +1 -0
- package/dist/esm/sources/EnvironmentSource.mjs +2 -0
- package/dist/esm/sources/EnvironmentSource.mjs.map +1 -0
- package/dist/esm/sources/MemorySource.mjs +2 -0
- package/dist/esm/sources/MemorySource.mjs.map +1 -0
- package/dist/esm/sources/RemoteSource.mjs +2 -0
- package/dist/esm/sources/RemoteSource.mjs.map +1 -0
- package/dist/esm/sources/helpers/loadJson.mjs +2 -0
- package/dist/esm/sources/helpers/loadJson.mjs.map +1 -0
- package/dist/production.analysis.txt +89 -0
- package/dist/types/cjs/__testUtils__/components/ErrorBoundary.d.cts +14 -0
- package/dist/types/cjs/__testUtils__/components/ErrorBoundary.d.cts.map +1 -0
- package/dist/types/cjs/cli/command.d.cts +7 -0
- package/dist/types/cjs/cli/command.d.cts.map +1 -0
- package/dist/types/cjs/cli/handler.d.cts +7 -0
- package/dist/types/cjs/cli/handler.d.cts.map +1 -0
- package/dist/types/cjs/cli.d.cts +2 -0
- package/dist/types/cjs/cli.d.cts.map +1 -0
- package/dist/types/cjs/components/AppSettingsProvider.d.cts +7 -0
- package/dist/types/cjs/components/AppSettingsProvider.d.cts.map +1 -0
- package/dist/types/cjs/components/Setting.d.cts +20 -0
- package/dist/types/cjs/components/Setting.d.cts.map +1 -0
- package/dist/types/cjs/config/AppSettingsImplementation.d.cts +15 -0
- package/dist/types/cjs/config/AppSettingsImplementation.d.cts.map +1 -0
- package/dist/types/cjs/config/ConfigMerger.d.cts +4 -0
- package/dist/types/cjs/config/ConfigMerger.d.cts.map +1 -0
- package/dist/types/cjs/config/ConfigReader.d.cts +14 -0
- package/dist/types/cjs/config/ConfigReader.d.cts.map +1 -0
- package/dist/types/cjs/config/ConfigStore.d.cts +6 -0
- package/dist/types/cjs/config/ConfigStore.d.cts.map +1 -0
- package/dist/types/cjs/config/ConfigValidator.d.cts +7 -0
- package/dist/types/cjs/config/ConfigValidator.d.cts.map +1 -0
- package/dist/types/cjs/config/ScopedSettingsImplementation.d.cts +13 -0
- package/dist/types/cjs/config/ScopedSettingsImplementation.d.cts.map +1 -0
- package/dist/types/cjs/config/createAppSettings.d.cts +3 -0
- package/dist/types/cjs/config/createAppSettings.d.cts.map +1 -0
- package/dist/types/cjs/config/helpers/freezeDeep.d.cts +2 -0
- package/dist/types/cjs/config/helpers/freezeDeep.d.cts.map +1 -0
- package/dist/types/cjs/hooks/useAppSetting.d.cts +6 -0
- package/dist/types/cjs/hooks/useAppSetting.d.cts.map +1 -0
- package/dist/types/cjs/hooks/useAppSettings.d.cts +6 -0
- package/dist/types/cjs/hooks/useAppSettings.d.cts.map +1 -0
- package/dist/types/cjs/hooks/useAppSettingsContext.d.cts +5 -0
- package/dist/types/cjs/hooks/useAppSettingsContext.d.cts.map +1 -0
- package/dist/types/cjs/index.d.cts +10 -0
- package/dist/types/cjs/index.d.cts.map +1 -0
- package/dist/types/cjs/server.d.cts +8 -0
- package/dist/types/cjs/server.d.cts.map +1 -0
- package/dist/types/cjs/sources/AppSettingsSource.d.cts +14 -0
- package/dist/types/cjs/sources/AppSettingsSource.d.cts.map +1 -0
- package/dist/types/cjs/sources/EnvironmentSource.d.cts +16 -0
- package/dist/types/cjs/sources/EnvironmentSource.d.cts.map +1 -0
- package/dist/types/cjs/sources/MemorySource.d.cts +12 -0
- package/dist/types/cjs/sources/MemorySource.d.cts.map +1 -0
- package/dist/types/cjs/sources/RemoteSource.d.cts +16 -0
- package/dist/types/cjs/sources/RemoteSource.d.cts.map +1 -0
- package/dist/types/cjs/sources/helpers/loadJson.d.cts +3 -0
- package/dist/types/cjs/sources/helpers/loadJson.d.cts.map +1 -0
- package/dist/types/cjs/types/AppSettings.types.d.cts +26 -0
- package/dist/types/cjs/types/AppSettings.types.d.cts.map +1 -0
- package/dist/types/cjs/types/Config.types.d.cts +12 -0
- package/dist/types/cjs/types/Config.types.d.cts.map +1 -0
- package/dist/types/cjs/types/Source.types.d.cts +5 -0
- package/dist/types/cjs/types/Source.types.d.cts.map +1 -0
- package/dist/types/esm/__testUtils__/components/ErrorBoundary.d.ts +14 -0
- package/dist/types/esm/__testUtils__/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/types/esm/cli/command.d.ts +7 -0
- package/dist/types/esm/cli/command.d.ts.map +1 -0
- package/dist/types/esm/cli/handler.d.ts +7 -0
- package/dist/types/esm/cli/handler.d.ts.map +1 -0
- package/dist/types/esm/cli.d.ts +2 -0
- package/dist/types/esm/cli.d.ts.map +1 -0
- package/dist/types/esm/components/AppSettingsProvider.d.ts +7 -0
- package/dist/types/esm/components/AppSettingsProvider.d.ts.map +1 -0
- package/dist/types/esm/components/Setting.d.ts +20 -0
- package/dist/types/esm/components/Setting.d.ts.map +1 -0
- package/dist/types/esm/config/AppSettingsImplementation.d.ts +15 -0
- package/dist/types/esm/config/AppSettingsImplementation.d.ts.map +1 -0
- package/dist/types/esm/config/ConfigMerger.d.ts +4 -0
- package/dist/types/esm/config/ConfigMerger.d.ts.map +1 -0
- package/dist/types/esm/config/ConfigReader.d.ts +14 -0
- package/dist/types/esm/config/ConfigReader.d.ts.map +1 -0
- package/dist/types/esm/config/ConfigStore.d.ts +6 -0
- package/dist/types/esm/config/ConfigStore.d.ts.map +1 -0
- package/dist/types/esm/config/ConfigValidator.d.ts +7 -0
- package/dist/types/esm/config/ConfigValidator.d.ts.map +1 -0
- package/dist/types/esm/config/ScopedSettingsImplementation.d.ts +13 -0
- package/dist/types/esm/config/ScopedSettingsImplementation.d.ts.map +1 -0
- package/dist/types/esm/config/createAppSettings.d.ts +3 -0
- package/dist/types/esm/config/createAppSettings.d.ts.map +1 -0
- package/dist/types/esm/config/helpers/freezeDeep.d.ts +2 -0
- package/dist/types/esm/config/helpers/freezeDeep.d.ts.map +1 -0
- package/dist/types/esm/hooks/useAppSetting.d.ts +6 -0
- package/dist/types/esm/hooks/useAppSetting.d.ts.map +1 -0
- package/dist/types/esm/hooks/useAppSettings.d.ts +6 -0
- package/dist/types/esm/hooks/useAppSettings.d.ts.map +1 -0
- package/dist/types/esm/hooks/useAppSettingsContext.d.ts +5 -0
- package/dist/types/esm/hooks/useAppSettingsContext.d.ts.map +1 -0
- package/dist/types/esm/index.d.ts +10 -0
- package/dist/types/esm/index.d.ts.map +1 -0
- package/dist/types/esm/server.d.ts +8 -0
- package/dist/types/esm/server.d.ts.map +1 -0
- package/dist/types/esm/sources/AppSettingsSource.d.ts +14 -0
- package/dist/types/esm/sources/AppSettingsSource.d.ts.map +1 -0
- package/dist/types/esm/sources/EnvironmentSource.d.ts +16 -0
- package/dist/types/esm/sources/EnvironmentSource.d.ts.map +1 -0
- package/dist/types/esm/sources/MemorySource.d.ts +12 -0
- package/dist/types/esm/sources/MemorySource.d.ts.map +1 -0
- package/dist/types/esm/sources/RemoteSource.d.ts +16 -0
- package/dist/types/esm/sources/RemoteSource.d.ts.map +1 -0
- package/dist/types/esm/sources/helpers/loadJson.d.ts +3 -0
- package/dist/types/esm/sources/helpers/loadJson.d.ts.map +1 -0
- package/dist/types/esm/types/AppSettings.types.d.ts +26 -0
- package/dist/types/esm/types/AppSettings.types.d.ts.map +1 -0
- package/dist/types/esm/types/Config.types.d.ts +12 -0
- package/dist/types/esm/types/Config.types.d.ts.map +1 -0
- package/dist/types/esm/types/Source.types.d.ts +5 -0
- package/dist/types/esm/types/Source.types.d.ts.map +1 -0
- package/dist/types/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +85 -3
- package/rollup.config.mjs +17 -0
- package/src/__testUtils__/AppSettingsSchema.d.ts +9 -0
- package/src/__testUtils__/components/ErrorBoundary.tsx +25 -0
- package/src/cli/command.test.ts +36 -0
- package/src/cli/command.ts +33 -0
- package/src/cli/handler.test.ts +61 -0
- package/src/cli/handler.ts +58 -0
- package/src/cli.test.ts +45 -0
- package/src/cli.ts +13 -0
- package/src/components/AppSettingsProvider.test.tsx +25 -0
- package/src/components/AppSettingsProvider.tsx +18 -0
- package/src/components/Setting.test.tsx +59 -0
- package/src/components/Setting.tsx +43 -0
- package/src/config/AppSettingsImplementation.test.ts +65 -0
- package/src/config/AppSettingsImplementation.ts +61 -0
- package/src/config/ConfigMerger.test.ts +44 -0
- package/src/config/ConfigMerger.ts +9 -0
- package/src/config/ConfigReader.test.ts +56 -0
- package/src/config/ConfigReader.ts +45 -0
- package/src/config/ConfigStore.test.ts +32 -0
- package/src/config/ConfigStore.ts +12 -0
- package/src/config/ConfigValidator.test.ts +30 -0
- package/src/config/ConfigValidator.ts +13 -0
- package/src/config/ScopedSettingsImplementation.test.ts +31 -0
- package/src/config/ScopedSettingsImplementation.ts +36 -0
- package/src/config/createAppSettings.test.ts +84 -0
- package/src/config/createAppSettings.ts +18 -0
- package/src/config/helpers/freezeDeep.test.ts +41 -0
- package/src/config/helpers/freezeDeep.ts +15 -0
- package/src/hooks/useAppSetting.test.tsx +37 -0
- package/src/hooks/useAppSetting.ts +14 -0
- package/src/hooks/useAppSettings.test.tsx +37 -0
- package/src/hooks/useAppSettings.ts +17 -0
- package/src/hooks/useAppSettingsContext.test.tsx +45 -0
- package/src/hooks/useAppSettingsContext.ts +17 -0
- package/src/index.ts +11 -0
- package/src/server.ts +7 -0
- package/src/sources/AppSettingsSource.test.ts +61 -0
- package/src/sources/AppSettingsSource.ts +30 -0
- package/src/sources/EnvironmentSource.test.ts +45 -0
- package/src/sources/EnvironmentSource.ts +87 -0
- package/src/sources/MemorySource.test.ts +31 -0
- package/src/sources/MemorySource.ts +21 -0
- package/src/sources/RemoteSource.test.ts +80 -0
- package/src/sources/RemoteSource.ts +50 -0
- package/src/sources/helpers/loadJson.test.ts +74 -0
- package/src/sources/helpers/loadJson.ts +29 -0
- package/src/types/AppSettings.types.test.ts +61 -0
- package/src/types/AppSettings.types.ts +30 -0
- package/src/types/Config.types.test.ts +36 -0
- package/src/types/Config.types.ts +27 -0
- package/src/types/Source.types.ts +4 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@next-box/appsettings",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "A configuration management library for TypeScript applications, inspired by .NET’s appsettings.json.",
|
|
4
|
+
"version": "0.0.4",
|
|
5
|
+
"author": "miami-man",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/badbatch/next-box",
|
|
8
|
+
"repository": {
|
|
9
|
+
"directory": "packages/appsettings",
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/badbatch/next-box.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/badbatch/next-box/issues",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"appsettings": "./bin/appsettings.mjs"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/cjs/index.cjs",
|
|
19
|
+
"module": "./dist/esm/index.mjs",
|
|
20
|
+
"types": "./dist/types/cjs/index.d.cts",
|
|
21
|
+
"imports": {
|
|
22
|
+
"#*": "./src/*"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"source": "./src/index.ts",
|
|
27
|
+
"types": {
|
|
28
|
+
"import": "./dist/types/esm/index.d.ts",
|
|
29
|
+
"require": "./dist/types/cjs/index.d.cts"
|
|
30
|
+
},
|
|
31
|
+
"import": "./dist/esm/index.mjs",
|
|
32
|
+
"require": "./dist/cjs/index.cjs"
|
|
33
|
+
},
|
|
34
|
+
"./cli": {
|
|
35
|
+
"source": "./src/cli/index.ts",
|
|
36
|
+
"types": {
|
|
37
|
+
"import": "./dist/types/esm/cli.d.ts",
|
|
38
|
+
"require": "./dist/types/cjs/cli.d.cts"
|
|
39
|
+
},
|
|
40
|
+
"import": "./dist/esm/cli.mjs",
|
|
41
|
+
"require": "./dist/cjs/cli.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./server": {
|
|
44
|
+
"source": "./src/server.ts",
|
|
45
|
+
"types": {
|
|
46
|
+
"import": "./dist/types/esm/server.d.ts",
|
|
47
|
+
"require": "./dist/types/cjs/server.d.cts"
|
|
48
|
+
},
|
|
49
|
+
"import": "./dist/esm/server.mjs",
|
|
50
|
+
"require": "./dist/cjs/server.cjs"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"remeda": "^2.58.0",
|
|
58
|
+
"yargs": "^18.2.0",
|
|
59
|
+
"zcb": "^0.3.10",
|
|
60
|
+
"zod": "^4.6.7"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"react": ">=19"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@jest/globals": "^30.4.1",
|
|
67
|
+
"@testing-library/react": "^16.3.3",
|
|
68
|
+
"@types/node": "^26.6.6",
|
|
69
|
+
"@types/react": "^19.3.0",
|
|
70
|
+
"@types/react-dom": "^19.3.0",
|
|
71
|
+
"@types/yargs": "^17.0.35",
|
|
72
|
+
"cts-types": "^0.0.17",
|
|
73
|
+
"del-cli": "^8.0.1",
|
|
74
|
+
"react": "^19.3.1",
|
|
75
|
+
"react-dom": "^19.3.1",
|
|
76
|
+
"type-fest": "^5.10.0"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "pnpm run clean:dist && pnpm run compile",
|
|
80
|
+
"clean:deps": "del-cli ./node_modules",
|
|
81
|
+
"clean:dist": "del-cli ./dist",
|
|
82
|
+
"compile": "pnpm run /^compile:.*/",
|
|
83
|
+
"compile:cjs": "MODULE_SYSTEM=cjs rollup -c ./rollup.config.mjs",
|
|
84
|
+
"compile:esm": "rollup -c ./rollup.config.mjs",
|
|
85
|
+
"compile:types": "tsc --project ./tsconfig.build.json && cts-types build dist/types/esm dist/types/cjs",
|
|
86
|
+
"type-check": "tsc --project ./tsconfig.json --noEmit --composite false --declaration false --declarationMap false --emitDeclarationOnly false"
|
|
87
|
+
}
|
|
6
88
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import config from '../../rollup.config.mjs';
|
|
2
|
+
|
|
3
|
+
const { input, ...rest } = config;
|
|
4
|
+
|
|
5
|
+
// rollup requires config to be default export
|
|
6
|
+
// eslint-disable-next-line import-x/no-default-export
|
|
7
|
+
export default [
|
|
8
|
+
config,
|
|
9
|
+
{
|
|
10
|
+
...rest,
|
|
11
|
+
input: input.replace('index', 'cli'),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
...rest,
|
|
15
|
+
input: input.replace('index', 'server'),
|
|
16
|
+
},
|
|
17
|
+
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Component, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ErrorBoundaryProps = {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type ErrorBoundaryState = {
|
|
8
|
+
error: Error | undefined;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
12
|
+
public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
13
|
+
return { error };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public override state: ErrorBoundaryState = { error: undefined };
|
|
17
|
+
|
|
18
|
+
public override render(): ReactNode {
|
|
19
|
+
if (this.state.error) {
|
|
20
|
+
return <div>{this.state.error.message}</div>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return this.props.children;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import yargs, { type Argv } from 'yargs';
|
|
3
|
+
import * as generateTypesCommand from './command.ts';
|
|
4
|
+
|
|
5
|
+
describe('generate-types command', () => {
|
|
6
|
+
it('When help is requested, then it displays the command usage and options', async () => {
|
|
7
|
+
// @ts-expect-error not important in test
|
|
8
|
+
const argv = generateTypesCommand.builder(yargs([])) as Argv;
|
|
9
|
+
|
|
10
|
+
// Arrange
|
|
11
|
+
const parser = argv
|
|
12
|
+
.scriptName('appsettings')
|
|
13
|
+
.usage(`$0 ${generateTypesCommand.command}`, generateTypesCommand.describe)
|
|
14
|
+
.help();
|
|
15
|
+
|
|
16
|
+
// Act
|
|
17
|
+
const help = await parser.getHelp();
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
expect(help).toMatchInlineSnapshot(`
|
|
21
|
+
"appsettings generate-types <schema> <output>
|
|
22
|
+
|
|
23
|
+
Generate a declaration that types appsettings hooks from your schema.
|
|
24
|
+
|
|
25
|
+
Positionals:
|
|
26
|
+
schema TypeScript module that exports the inferred settings type. [string]
|
|
27
|
+
output Declaration file to generate.
|
|
28
|
+
[string] [default: "appsettings.generated.d.ts"]
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--version Show version number [boolean]
|
|
32
|
+
--type Exported type name. [string] [default: "AppSettingsConfig"]
|
|
33
|
+
--help Show help [boolean]"
|
|
34
|
+
`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type CommandBuilder } from 'yargs';
|
|
2
|
+
import { type GenerateTypesArguments } from '#cli/handler.ts';
|
|
3
|
+
|
|
4
|
+
export const builder: CommandBuilder<object, GenerateTypesArguments> = yargs =>
|
|
5
|
+
yargs
|
|
6
|
+
.positional('schema', {
|
|
7
|
+
demandOption: true,
|
|
8
|
+
describe: 'TypeScript module that exports the inferred settings type.',
|
|
9
|
+
type: 'string',
|
|
10
|
+
})
|
|
11
|
+
.positional('output', {
|
|
12
|
+
default: 'appsettings.generated.d.ts',
|
|
13
|
+
describe: 'Declaration file to generate.',
|
|
14
|
+
type: 'string',
|
|
15
|
+
})
|
|
16
|
+
.option('type', {
|
|
17
|
+
default: 'AppSettingsConfig',
|
|
18
|
+
describe: 'Exported type name.',
|
|
19
|
+
type: 'string',
|
|
20
|
+
})
|
|
21
|
+
.check(({ type }) => {
|
|
22
|
+
if (!/^[A-Za-z_$][\w$]*$/u.test(type)) {
|
|
23
|
+
throw new Error('--type must be a TypeScript identifier.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const command = 'generate-types <schema> <output>';
|
|
30
|
+
|
|
31
|
+
export const describe = 'Generate a declaration that types appsettings hooks from your schema.';
|
|
32
|
+
|
|
33
|
+
export { handler } from './handler.ts';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { handler } from './handler.ts';
|
|
6
|
+
|
|
7
|
+
describe('generate-types handler', () => {
|
|
8
|
+
let directory: string | undefined;
|
|
9
|
+
let originalWorkingDirectory: string | undefined;
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
if (originalWorkingDirectory) {
|
|
13
|
+
process.chdir(originalWorkingDirectory);
|
|
14
|
+
originalWorkingDirectory = undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (directory) {
|
|
18
|
+
await rm(directory, { force: true, recursive: true });
|
|
19
|
+
directory = undefined;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('When called with schema and output paths, then it writes a relative type augmentation declaration', async () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
directory = await mkdtemp(join(tmpdir(), 'appsettings-cli-'));
|
|
26
|
+
originalWorkingDirectory = process.cwd();
|
|
27
|
+
process.chdir(directory);
|
|
28
|
+
|
|
29
|
+
const args = {
|
|
30
|
+
output: 'src/types/appsettings.generated.d.ts',
|
|
31
|
+
schema: 'src/appSettings.schema.ts',
|
|
32
|
+
type: 'AppSettingsConfig',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
await handler(args);
|
|
37
|
+
const contents = await readFile(args.output, 'utf8');
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(contents).toContain("import type { AppSettingsConfig } from '../appSettings.schema';");
|
|
41
|
+
expect(contents).toContain("declare module '@next-box/appsettings'");
|
|
42
|
+
expect(contents).toContain('interface AppSettingsSchema extends AppSettingsConfig {}');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('When called again without changing its arguments, then it keeps the generated file contents unchanged', async () => {
|
|
46
|
+
// Arrange
|
|
47
|
+
directory = await mkdtemp(join(tmpdir(), 'appsettings-cli-'));
|
|
48
|
+
originalWorkingDirectory = process.cwd();
|
|
49
|
+
process.chdir(directory);
|
|
50
|
+
const args = { output: 'generated.d.ts', schema: 'schema.ts', type: 'Settings' };
|
|
51
|
+
await handler(args);
|
|
52
|
+
const firstContents = await readFile(args.output, 'utf8');
|
|
53
|
+
|
|
54
|
+
// Act
|
|
55
|
+
await handler(args);
|
|
56
|
+
const secondContents = await readFile(args.output, 'utf8');
|
|
57
|
+
|
|
58
|
+
// Assert
|
|
59
|
+
expect(secondContents).toBe(firstContents);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, relative, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const createModuleSpecifier = (path: string): string => {
|
|
5
|
+
let specifier = path
|
|
6
|
+
.split(sep)
|
|
7
|
+
.join('/')
|
|
8
|
+
.replace(/\.(?:[cm]?tsx?)$/u, '');
|
|
9
|
+
|
|
10
|
+
if (!specifier.startsWith('.')) {
|
|
11
|
+
specifier = `./${specifier}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return specifier;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const isMissingFile = (error: unknown): boolean =>
|
|
18
|
+
!!error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
|
|
19
|
+
|
|
20
|
+
export interface GenerateTypesArguments {
|
|
21
|
+
output: string;
|
|
22
|
+
schema: string;
|
|
23
|
+
type: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const handler = async ({ output, schema, type }: GenerateTypesArguments): Promise<void> => {
|
|
27
|
+
const schemaPath = resolve(process.cwd(), schema);
|
|
28
|
+
const outputPath = resolve(process.cwd(), output);
|
|
29
|
+
const schemaImportPath = createModuleSpecifier(relative(dirname(outputPath), schemaPath));
|
|
30
|
+
|
|
31
|
+
const contents = [
|
|
32
|
+
`import type { ${type} } from '${schemaImportPath}';`,
|
|
33
|
+
'',
|
|
34
|
+
"declare module '@next-box/appsettings' {",
|
|
35
|
+
` interface AppSettingsSchema extends ${type} {}`,
|
|
36
|
+
'}',
|
|
37
|
+
'',
|
|
38
|
+
'export {};',
|
|
39
|
+
'',
|
|
40
|
+
].join('\n');
|
|
41
|
+
|
|
42
|
+
let existingContents: string | undefined;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
existingContents = await readFile(outputPath, 'utf8');
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!isMissingFile(error)) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (existingContents !== contents) {
|
|
53
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
54
|
+
await writeFile(outputPath, contents, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.stdout.write(`Generated ${relative(process.cwd(), outputPath)}\n`);
|
|
58
|
+
};
|
package/src/cli.test.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { runAppsettingsCli } from './cli.ts';
|
|
6
|
+
|
|
7
|
+
describe('runAppsettingsCli', () => {
|
|
8
|
+
let directory: string | undefined;
|
|
9
|
+
let originalWorkingDirectory: string | undefined;
|
|
10
|
+
let originalArguments: string[] | undefined;
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
if (originalArguments) {
|
|
14
|
+
process.argv = originalArguments;
|
|
15
|
+
originalArguments = undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (originalWorkingDirectory) {
|
|
19
|
+
process.chdir(originalWorkingDirectory);
|
|
20
|
+
originalWorkingDirectory = undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (directory) {
|
|
24
|
+
await rm(directory, { force: true, recursive: true });
|
|
25
|
+
directory = undefined;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('When invoked with generate-types arguments, then it runs the command and writes the declaration', async () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
directory = await mkdtemp(join(tmpdir(), 'appsettings-cli-'));
|
|
32
|
+
originalWorkingDirectory = process.cwd();
|
|
33
|
+
originalArguments = process.argv;
|
|
34
|
+
process.chdir(directory);
|
|
35
|
+
process.argv = ['node', 'appsettings', 'generate-types', 'schema.ts', 'generated.d.ts', '--type', 'Settings'];
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
await runAppsettingsCli();
|
|
39
|
+
const contents = await readFile('generated.d.ts', 'utf8');
|
|
40
|
+
|
|
41
|
+
// Assert
|
|
42
|
+
expect(contents).toContain("import type { Settings } from './schema';");
|
|
43
|
+
expect(contents).toContain('interface AppSettingsSchema extends Settings {}');
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
import * as generateTypesCommand from './cli/command.ts';
|
|
4
|
+
|
|
5
|
+
export const runAppsettingsCli = async (): Promise<void> => {
|
|
6
|
+
await yargs(hideBin(process.argv))
|
|
7
|
+
.scriptName('appsettings')
|
|
8
|
+
.command(generateTypesCommand)
|
|
9
|
+
.demandCommand(1, 'Choose a command to run.')
|
|
10
|
+
.strict()
|
|
11
|
+
.help()
|
|
12
|
+
.parseAsync();
|
|
13
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @jest-environment @repodog/jest-config/FailOnJsdomConsoleEnvironment.mjs */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from '@jest/globals';
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { useAppSetting } from '../hooks/useAppSetting.ts';
|
|
6
|
+
import { AppSettingsProvider } from './AppSettingsProvider.tsx';
|
|
7
|
+
|
|
8
|
+
const SettingsOutput = () => <span>{String(useAppSetting('Api.Timeout'))}</span>;
|
|
9
|
+
|
|
10
|
+
describe('AppSettingsProvider', () => {
|
|
11
|
+
it('When settings are provided, then child components can read the supplied snapshot', () => {
|
|
12
|
+
// Arrange
|
|
13
|
+
const settings = { Api: { Timeout: 5000 } };
|
|
14
|
+
|
|
15
|
+
// Act
|
|
16
|
+
render(
|
|
17
|
+
<AppSettingsProvider settings={settings}>
|
|
18
|
+
<SettingsOutput />
|
|
19
|
+
</AppSettingsProvider>,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Assert
|
|
23
|
+
expect(screen.getByText('5000')).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type ReactElement, type ReactNode, useMemo } from 'react';
|
|
4
|
+
import { AppSettingsImplementation } from '#config/AppSettingsImplementation.ts';
|
|
5
|
+
import { AppSettingsContext } from '../hooks/useAppSettingsContext.ts';
|
|
6
|
+
|
|
7
|
+
export interface AppSettingsProviderProps<T extends object = object> {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
settings: Readonly<T>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const AppSettingsProvider = <T extends object>({
|
|
13
|
+
children,
|
|
14
|
+
settings,
|
|
15
|
+
}: AppSettingsProviderProps<T>): ReactElement => {
|
|
16
|
+
const appSettings = useMemo(() => new AppSettingsImplementation(settings), [settings]);
|
|
17
|
+
return <AppSettingsContext value={appSettings}>{children}</AppSettingsContext>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** @jest-environment @repodog/jest-config/FailOnJsdomConsoleEnvironment.mjs */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from '@jest/globals';
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { AppSettingsProvider } from './AppSettingsProvider.tsx';
|
|
6
|
+
import { Setting } from './Setting.tsx';
|
|
7
|
+
|
|
8
|
+
describe('Setting', () => {
|
|
9
|
+
it('When the current value matches one of the expected values, then it renders the children', () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
const settings = { Features: { DarkMode: true } };
|
|
12
|
+
|
|
13
|
+
// Act
|
|
14
|
+
render(
|
|
15
|
+
<AppSettingsProvider settings={settings}>
|
|
16
|
+
<Setting path="Features.DarkMode" value={[false, true]}>
|
|
17
|
+
Enabled
|
|
18
|
+
</Setting>
|
|
19
|
+
</AppSettingsProvider>,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Assert
|
|
23
|
+
expect(screen.getByText('Enabled')).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('When the value does not match, then it renders the fallback', () => {
|
|
27
|
+
// Arrange
|
|
28
|
+
const settings = { Features: { DarkMode: false } };
|
|
29
|
+
|
|
30
|
+
// Act
|
|
31
|
+
render(
|
|
32
|
+
<AppSettingsProvider settings={settings}>
|
|
33
|
+
<Setting fallback="Disabled" path="Features.DarkMode" value={true}>
|
|
34
|
+
Enabled
|
|
35
|
+
</Setting>
|
|
36
|
+
</AppSettingsProvider>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(screen.getByText('Disabled')).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('When negate is enabled, then it renders children for a non-matching value', () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const settings = { Features: { DarkMode: false } };
|
|
46
|
+
|
|
47
|
+
// Act
|
|
48
|
+
render(
|
|
49
|
+
<AppSettingsProvider settings={settings}>
|
|
50
|
+
<Setting negate path="Features.DarkMode" value={true}>
|
|
51
|
+
Not enabled
|
|
52
|
+
</Setting>
|
|
53
|
+
</AppSettingsProvider>,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(screen.getByText('Not enabled')).toBeTruthy();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type ReactNode } from 'react';
|
|
4
|
+
import { type Get } from 'type-fest';
|
|
5
|
+
import { useAppSettingsContext } from '#hooks/useAppSettingsContext.ts';
|
|
6
|
+
import { type AppSettingsSchema } from '../index.ts';
|
|
7
|
+
import { type SettingPath } from '../types/Config.types.ts';
|
|
8
|
+
|
|
9
|
+
export interface SettingProps<T extends object, P extends SettingPath<T>> {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
fallback?: ReactNode;
|
|
12
|
+
negate?: boolean;
|
|
13
|
+
path: P;
|
|
14
|
+
value: Get<T, P> | undefined | readonly (Get<T, P> | undefined)[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type SettingRuntimeProps = {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
fallback?: ReactNode;
|
|
20
|
+
negate?: boolean;
|
|
21
|
+
path: string;
|
|
22
|
+
value: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function Setting<P extends SettingPath<AppSettingsSchema>>(props: {
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
fallback?: ReactNode;
|
|
28
|
+
negate?: boolean;
|
|
29
|
+
path: P;
|
|
30
|
+
value: Get<AppSettingsSchema, P> | undefined | readonly (Get<AppSettingsSchema, P> | undefined)[];
|
|
31
|
+
}): ReactNode;
|
|
32
|
+
|
|
33
|
+
export function Setting<T extends object, P extends SettingPath<T>>(props: SettingProps<T, P>): ReactNode;
|
|
34
|
+
|
|
35
|
+
// Required to support above overloads
|
|
36
|
+
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
|
37
|
+
export function Setting({ children, fallback, negate = false, path, value }: SettingRuntimeProps): ReactNode {
|
|
38
|
+
const settings = useAppSettingsContext();
|
|
39
|
+
const current = settings.read(path);
|
|
40
|
+
const expected = Array.isArray(value) ? value : [value];
|
|
41
|
+
const isMatches = expected.includes(current);
|
|
42
|
+
return (negate ? !isMatches : isMatches) ? children : fallback;
|
|
43
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { AppSettingsImplementation } from './AppSettingsImplementation.ts';
|
|
3
|
+
|
|
4
|
+
describe('AppSettingsImplementation', () => {
|
|
5
|
+
it('When reading configured paths, then it returns leaves, sections, and the complete snapshot', () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const settings = new AppSettingsImplementation({
|
|
8
|
+
Api: { Retry: undefined, Timeout: 5000 },
|
|
9
|
+
Features: { DarkMode: true },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Act
|
|
13
|
+
const leaf = settings.get('Api.Timeout');
|
|
14
|
+
const section = settings.getSection('Api');
|
|
15
|
+
const all = settings.getAll();
|
|
16
|
+
|
|
17
|
+
// Assert
|
|
18
|
+
expect(leaf).toBe(5000);
|
|
19
|
+
expect(section).toEqual({ Timeout: 5000 });
|
|
20
|
+
expect(all).toEqual({ Api: { Timeout: 5000 }, Features: { DarkMode: true } });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('When a required setting exists or is missing, then it returns the value or throws with its path', () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const settings = new AppSettingsImplementation({ Api: { Retry: undefined, Timeout: 5000 } });
|
|
26
|
+
|
|
27
|
+
// Act
|
|
28
|
+
const value = settings.getOrThrow('Api.Timeout');
|
|
29
|
+
|
|
30
|
+
const getMissingValue = () => {
|
|
31
|
+
settings.getOrThrow('Api.Retry');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(value).toBe(5000);
|
|
36
|
+
expect(getMissingValue).toThrow('Required app setting "Api.Retry" is missing.');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('When checking paths, then it reports present values and sections but not missing values', () => {
|
|
40
|
+
// Arrange
|
|
41
|
+
const settings = new AppSettingsImplementation({ Api: { Retry: undefined, Timeout: 5000 } });
|
|
42
|
+
|
|
43
|
+
// Act
|
|
44
|
+
const hasLeaf = settings.has('Api.Timeout');
|
|
45
|
+
const hasSection = settings.has('Api');
|
|
46
|
+
const hasMissing = settings.has('Api.Retry');
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(hasLeaf).toBe(true);
|
|
50
|
+
expect(hasSection).toBe(true);
|
|
51
|
+
expect(hasMissing).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('When a plain object section is scoped, then it returns a reader relative to that section', () => {
|
|
55
|
+
// Arrange
|
|
56
|
+
const settings = new AppSettingsImplementation({ Api: { Timeout: 5000 } });
|
|
57
|
+
|
|
58
|
+
// Act
|
|
59
|
+
const scoped = settings.scope('Api');
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect(scoped.get('Timeout')).toBe(5000);
|
|
63
|
+
expect(scoped.getOrThrow('Timeout')).toBe(5000);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isPlainObject } from 'remeda';
|
|
2
|
+
import { type Get } from 'type-fest';
|
|
3
|
+
import { ScopedSettingsImplementation } from '#config/ScopedSettingsImplementation.ts';
|
|
4
|
+
import { type InternalAppSettings, type ScopedSettings } from '#types/AppSettings.types.ts';
|
|
5
|
+
import { type SettingPath, type SettingSectionPath } from '#types/Config.types.ts';
|
|
6
|
+
import { ConfigReader } from './ConfigReader.ts';
|
|
7
|
+
import { ConfigStore } from './ConfigStore.ts';
|
|
8
|
+
|
|
9
|
+
export class AppSettingsImplementation<T extends object> implements InternalAppSettings<T> {
|
|
10
|
+
private readonly store: ConfigStore<T>;
|
|
11
|
+
|
|
12
|
+
constructor(settings: T) {
|
|
13
|
+
this.store = new ConfigStore(settings);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get<P extends SettingPath<T>>(path: P): Get<T, P> | undefined;
|
|
17
|
+
get(path: string): unknown {
|
|
18
|
+
return this.read(path);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
read(path: string): unknown {
|
|
22
|
+
return new ConfigReader(this.store.getSnapshot()).read(path);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getAll(): Readonly<T> {
|
|
26
|
+
return this.store.getSnapshot();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getOrThrow<P extends SettingPath<T>>(path: P): Get<T, P>;
|
|
30
|
+
getOrThrow(path: string): unknown {
|
|
31
|
+
const value = new ConfigReader(this.store.getSnapshot()).read(path);
|
|
32
|
+
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
throw new Error(`Required app setting "${path}" is missing.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getSection<P extends SettingSectionPath<T>>(path: P): Get<T, P> | undefined;
|
|
41
|
+
getSection(path: string): unknown {
|
|
42
|
+
return new ConfigReader(this.store.getSnapshot()).read(path);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
has(path: SettingPath<T> | SettingSectionPath<T>): boolean;
|
|
46
|
+
has(path: string): boolean {
|
|
47
|
+
return new ConfigReader(this.store.getSnapshot()).has(path);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
scope<P extends SettingSectionPath<T>>(path: P): ScopedSettings<Extract<Get<T, P>, object>>;
|
|
51
|
+
scope(path: string): ScopedSettings<object> {
|
|
52
|
+
const reader = new ConfigReader(this.store.getSnapshot());
|
|
53
|
+
const section = reader.read(path);
|
|
54
|
+
|
|
55
|
+
if (!isPlainObject(section)) {
|
|
56
|
+
throw new Error(`App setting section "${path}" is missing.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return new ScopedSettingsImplementation(section, path);
|
|
60
|
+
}
|
|
61
|
+
}
|