@next-box/appsettings 0.0.0-stage → 0.0.2

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.
Files changed (260) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -2
  3. package/bin/appsettings.mjs +7 -0
  4. package/dist/cjs/cli/cli/command.cjs +2 -0
  5. package/dist/cjs/cli/cli/command.cjs.map +1 -0
  6. package/dist/cjs/cli/cli/handler.cjs +2 -0
  7. package/dist/cjs/cli/cli/handler.cjs.map +1 -0
  8. package/dist/cjs/cli/cli/index.cjs +2 -0
  9. package/dist/cjs/cli/cli/index.cjs.map +1 -0
  10. package/dist/cjs/components/AppSettingsProvider.cjs +3 -0
  11. package/dist/cjs/components/AppSettingsProvider.cjs.map +1 -0
  12. package/dist/cjs/components/Setting.cjs +3 -0
  13. package/dist/cjs/components/Setting.cjs.map +1 -0
  14. package/dist/cjs/config/AppSettingsImplementation.cjs +2 -0
  15. package/dist/cjs/config/AppSettingsImplementation.cjs.map +1 -0
  16. package/dist/cjs/config/ConfigReader.cjs +2 -0
  17. package/dist/cjs/config/ConfigReader.cjs.map +1 -0
  18. package/dist/cjs/config/ConfigStore.cjs +2 -0
  19. package/dist/cjs/config/ConfigStore.cjs.map +1 -0
  20. package/dist/cjs/config/ScopedSettingsImplementation.cjs +2 -0
  21. package/dist/cjs/config/ScopedSettingsImplementation.cjs.map +1 -0
  22. package/dist/cjs/config/helpers/freezeDeep.cjs +2 -0
  23. package/dist/cjs/config/helpers/freezeDeep.cjs.map +1 -0
  24. package/dist/cjs/hooks/useAppSetting.cjs +2 -0
  25. package/dist/cjs/hooks/useAppSetting.cjs.map +1 -0
  26. package/dist/cjs/hooks/useAppSettings.cjs +2 -0
  27. package/dist/cjs/hooks/useAppSettings.cjs.map +1 -0
  28. package/dist/cjs/hooks/useAppSettingsContext.cjs +2 -0
  29. package/dist/cjs/hooks/useAppSettingsContext.cjs.map +1 -0
  30. package/dist/cjs/index.cjs +2 -0
  31. package/dist/cjs/index.cjs.map +1 -0
  32. package/dist/cjs/server/config/ConfigMerger.cjs +2 -0
  33. package/dist/cjs/server/config/ConfigMerger.cjs.map +1 -0
  34. package/dist/cjs/server/config/ConfigValidator.cjs +2 -0
  35. package/dist/cjs/server/config/ConfigValidator.cjs.map +1 -0
  36. package/dist/cjs/server/config/createAppSettings.cjs +2 -0
  37. package/dist/cjs/server/config/createAppSettings.cjs.map +1 -0
  38. package/dist/cjs/server/server.cjs +2 -0
  39. package/dist/cjs/server/server.cjs.map +1 -0
  40. package/dist/cjs/server/sources/AppSettingsSource.cjs +2 -0
  41. package/dist/cjs/server/sources/AppSettingsSource.cjs.map +1 -0
  42. package/dist/cjs/server/sources/EnvironmentSource.cjs +2 -0
  43. package/dist/cjs/server/sources/EnvironmentSource.cjs.map +1 -0
  44. package/dist/cjs/server/sources/MemorySource.cjs +2 -0
  45. package/dist/cjs/server/sources/MemorySource.cjs.map +1 -0
  46. package/dist/cjs/server/sources/RemoteSource.cjs +2 -0
  47. package/dist/cjs/server/sources/RemoteSource.cjs.map +1 -0
  48. package/dist/cjs/server/sources/helpers/loadJson.cjs +2 -0
  49. package/dist/cjs/server/sources/helpers/loadJson.cjs.map +1 -0
  50. package/dist/esm/cli/cli/command.mjs +2 -0
  51. package/dist/esm/cli/cli/command.mjs.map +1 -0
  52. package/dist/esm/cli/cli/handler.mjs +2 -0
  53. package/dist/esm/cli/cli/handler.mjs.map +1 -0
  54. package/dist/esm/cli/cli/index.mjs +2 -0
  55. package/dist/esm/cli/cli/index.mjs.map +1 -0
  56. package/dist/esm/components/AppSettingsProvider.mjs +3 -0
  57. package/dist/esm/components/AppSettingsProvider.mjs.map +1 -0
  58. package/dist/esm/components/Setting.mjs +3 -0
  59. package/dist/esm/components/Setting.mjs.map +1 -0
  60. package/dist/esm/config/AppSettingsImplementation.mjs +2 -0
  61. package/dist/esm/config/AppSettingsImplementation.mjs.map +1 -0
  62. package/dist/esm/config/ConfigReader.mjs +2 -0
  63. package/dist/esm/config/ConfigReader.mjs.map +1 -0
  64. package/dist/esm/config/ConfigStore.mjs +2 -0
  65. package/dist/esm/config/ConfigStore.mjs.map +1 -0
  66. package/dist/esm/config/ScopedSettingsImplementation.mjs +2 -0
  67. package/dist/esm/config/ScopedSettingsImplementation.mjs.map +1 -0
  68. package/dist/esm/config/helpers/freezeDeep.mjs +2 -0
  69. package/dist/esm/config/helpers/freezeDeep.mjs.map +1 -0
  70. package/dist/esm/hooks/useAppSetting.mjs +2 -0
  71. package/dist/esm/hooks/useAppSetting.mjs.map +1 -0
  72. package/dist/esm/hooks/useAppSettings.mjs +2 -0
  73. package/dist/esm/hooks/useAppSettings.mjs.map +1 -0
  74. package/dist/esm/hooks/useAppSettingsContext.mjs +2 -0
  75. package/dist/esm/hooks/useAppSettingsContext.mjs.map +1 -0
  76. package/dist/esm/index.mjs +2 -0
  77. package/dist/esm/index.mjs.map +1 -0
  78. package/dist/esm/server/config/ConfigMerger.mjs +2 -0
  79. package/dist/esm/server/config/ConfigMerger.mjs.map +1 -0
  80. package/dist/esm/server/config/ConfigValidator.mjs +2 -0
  81. package/dist/esm/server/config/ConfigValidator.mjs.map +1 -0
  82. package/dist/esm/server/config/createAppSettings.mjs +2 -0
  83. package/dist/esm/server/config/createAppSettings.mjs.map +1 -0
  84. package/dist/esm/server/server.mjs +2 -0
  85. package/dist/esm/server/server.mjs.map +1 -0
  86. package/dist/esm/server/sources/AppSettingsSource.mjs +2 -0
  87. package/dist/esm/server/sources/AppSettingsSource.mjs.map +1 -0
  88. package/dist/esm/server/sources/EnvironmentSource.mjs +2 -0
  89. package/dist/esm/server/sources/EnvironmentSource.mjs.map +1 -0
  90. package/dist/esm/server/sources/MemorySource.mjs +2 -0
  91. package/dist/esm/server/sources/MemorySource.mjs.map +1 -0
  92. package/dist/esm/server/sources/RemoteSource.mjs +2 -0
  93. package/dist/esm/server/sources/RemoteSource.mjs.map +1 -0
  94. package/dist/esm/server/sources/helpers/loadJson.mjs +2 -0
  95. package/dist/esm/server/sources/helpers/loadJson.mjs.map +1 -0
  96. package/dist/production.analysis.txt +89 -0
  97. package/dist/types/cjs/__testUtils__/components/ErrorBoundary.d.cts +14 -0
  98. package/dist/types/cjs/__testUtils__/components/ErrorBoundary.d.cts.map +1 -0
  99. package/dist/types/cjs/cli/command.d.cts +7 -0
  100. package/dist/types/cjs/cli/command.d.cts.map +1 -0
  101. package/dist/types/cjs/cli/handler.d.cts +7 -0
  102. package/dist/types/cjs/cli/handler.d.cts.map +1 -0
  103. package/dist/types/cjs/cli/index.d.cts +2 -0
  104. package/dist/types/cjs/cli/index.d.cts.map +1 -0
  105. package/dist/types/cjs/components/AppSettingsProvider.d.cts +7 -0
  106. package/dist/types/cjs/components/AppSettingsProvider.d.cts.map +1 -0
  107. package/dist/types/cjs/components/Setting.d.cts +20 -0
  108. package/dist/types/cjs/components/Setting.d.cts.map +1 -0
  109. package/dist/types/cjs/config/AppSettingsImplementation.d.cts +15 -0
  110. package/dist/types/cjs/config/AppSettingsImplementation.d.cts.map +1 -0
  111. package/dist/types/cjs/config/ConfigMerger.d.cts +4 -0
  112. package/dist/types/cjs/config/ConfigMerger.d.cts.map +1 -0
  113. package/dist/types/cjs/config/ConfigReader.d.cts +14 -0
  114. package/dist/types/cjs/config/ConfigReader.d.cts.map +1 -0
  115. package/dist/types/cjs/config/ConfigStore.d.cts +6 -0
  116. package/dist/types/cjs/config/ConfigStore.d.cts.map +1 -0
  117. package/dist/types/cjs/config/ConfigValidator.d.cts +7 -0
  118. package/dist/types/cjs/config/ConfigValidator.d.cts.map +1 -0
  119. package/dist/types/cjs/config/ScopedSettingsImplementation.d.cts +13 -0
  120. package/dist/types/cjs/config/ScopedSettingsImplementation.d.cts.map +1 -0
  121. package/dist/types/cjs/config/createAppSettings.d.cts +3 -0
  122. package/dist/types/cjs/config/createAppSettings.d.cts.map +1 -0
  123. package/dist/types/cjs/config/helpers/freezeDeep.d.cts +2 -0
  124. package/dist/types/cjs/config/helpers/freezeDeep.d.cts.map +1 -0
  125. package/dist/types/cjs/hooks/useAppSetting.d.cts +6 -0
  126. package/dist/types/cjs/hooks/useAppSetting.d.cts.map +1 -0
  127. package/dist/types/cjs/hooks/useAppSettings.d.cts +6 -0
  128. package/dist/types/cjs/hooks/useAppSettings.d.cts.map +1 -0
  129. package/dist/types/cjs/hooks/useAppSettingsContext.d.cts +5 -0
  130. package/dist/types/cjs/hooks/useAppSettingsContext.d.cts.map +1 -0
  131. package/dist/types/cjs/index.d.cts +10 -0
  132. package/dist/types/cjs/index.d.cts.map +1 -0
  133. package/dist/types/cjs/server.d.cts +8 -0
  134. package/dist/types/cjs/server.d.cts.map +1 -0
  135. package/dist/types/cjs/sources/AppSettingsSource.d.cts +14 -0
  136. package/dist/types/cjs/sources/AppSettingsSource.d.cts.map +1 -0
  137. package/dist/types/cjs/sources/EnvironmentSource.d.cts +16 -0
  138. package/dist/types/cjs/sources/EnvironmentSource.d.cts.map +1 -0
  139. package/dist/types/cjs/sources/MemorySource.d.cts +12 -0
  140. package/dist/types/cjs/sources/MemorySource.d.cts.map +1 -0
  141. package/dist/types/cjs/sources/RemoteSource.d.cts +16 -0
  142. package/dist/types/cjs/sources/RemoteSource.d.cts.map +1 -0
  143. package/dist/types/cjs/sources/helpers/loadJson.d.cts +3 -0
  144. package/dist/types/cjs/sources/helpers/loadJson.d.cts.map +1 -0
  145. package/dist/types/cjs/types/AppSettings.types.d.cts +26 -0
  146. package/dist/types/cjs/types/AppSettings.types.d.cts.map +1 -0
  147. package/dist/types/cjs/types/Config.types.d.cts +12 -0
  148. package/dist/types/cjs/types/Config.types.d.cts.map +1 -0
  149. package/dist/types/cjs/types/Source.types.d.cts +5 -0
  150. package/dist/types/cjs/types/Source.types.d.cts.map +1 -0
  151. package/dist/types/esm/__testUtils__/components/ErrorBoundary.d.ts +14 -0
  152. package/dist/types/esm/__testUtils__/components/ErrorBoundary.d.ts.map +1 -0
  153. package/dist/types/esm/cli/command.d.ts +7 -0
  154. package/dist/types/esm/cli/command.d.ts.map +1 -0
  155. package/dist/types/esm/cli/handler.d.ts +7 -0
  156. package/dist/types/esm/cli/handler.d.ts.map +1 -0
  157. package/dist/types/esm/cli/index.d.ts +2 -0
  158. package/dist/types/esm/cli/index.d.ts.map +1 -0
  159. package/dist/types/esm/components/AppSettingsProvider.d.ts +7 -0
  160. package/dist/types/esm/components/AppSettingsProvider.d.ts.map +1 -0
  161. package/dist/types/esm/components/Setting.d.ts +20 -0
  162. package/dist/types/esm/components/Setting.d.ts.map +1 -0
  163. package/dist/types/esm/config/AppSettingsImplementation.d.ts +15 -0
  164. package/dist/types/esm/config/AppSettingsImplementation.d.ts.map +1 -0
  165. package/dist/types/esm/config/ConfigMerger.d.ts +4 -0
  166. package/dist/types/esm/config/ConfigMerger.d.ts.map +1 -0
  167. package/dist/types/esm/config/ConfigReader.d.ts +14 -0
  168. package/dist/types/esm/config/ConfigReader.d.ts.map +1 -0
  169. package/dist/types/esm/config/ConfigStore.d.ts +6 -0
  170. package/dist/types/esm/config/ConfigStore.d.ts.map +1 -0
  171. package/dist/types/esm/config/ConfigValidator.d.ts +7 -0
  172. package/dist/types/esm/config/ConfigValidator.d.ts.map +1 -0
  173. package/dist/types/esm/config/ScopedSettingsImplementation.d.ts +13 -0
  174. package/dist/types/esm/config/ScopedSettingsImplementation.d.ts.map +1 -0
  175. package/dist/types/esm/config/createAppSettings.d.ts +3 -0
  176. package/dist/types/esm/config/createAppSettings.d.ts.map +1 -0
  177. package/dist/types/esm/config/helpers/freezeDeep.d.ts +2 -0
  178. package/dist/types/esm/config/helpers/freezeDeep.d.ts.map +1 -0
  179. package/dist/types/esm/hooks/useAppSetting.d.ts +6 -0
  180. package/dist/types/esm/hooks/useAppSetting.d.ts.map +1 -0
  181. package/dist/types/esm/hooks/useAppSettings.d.ts +6 -0
  182. package/dist/types/esm/hooks/useAppSettings.d.ts.map +1 -0
  183. package/dist/types/esm/hooks/useAppSettingsContext.d.ts +5 -0
  184. package/dist/types/esm/hooks/useAppSettingsContext.d.ts.map +1 -0
  185. package/dist/types/esm/index.d.ts +10 -0
  186. package/dist/types/esm/index.d.ts.map +1 -0
  187. package/dist/types/esm/server.d.ts +8 -0
  188. package/dist/types/esm/server.d.ts.map +1 -0
  189. package/dist/types/esm/sources/AppSettingsSource.d.ts +14 -0
  190. package/dist/types/esm/sources/AppSettingsSource.d.ts.map +1 -0
  191. package/dist/types/esm/sources/EnvironmentSource.d.ts +16 -0
  192. package/dist/types/esm/sources/EnvironmentSource.d.ts.map +1 -0
  193. package/dist/types/esm/sources/MemorySource.d.ts +12 -0
  194. package/dist/types/esm/sources/MemorySource.d.ts.map +1 -0
  195. package/dist/types/esm/sources/RemoteSource.d.ts +16 -0
  196. package/dist/types/esm/sources/RemoteSource.d.ts.map +1 -0
  197. package/dist/types/esm/sources/helpers/loadJson.d.ts +3 -0
  198. package/dist/types/esm/sources/helpers/loadJson.d.ts.map +1 -0
  199. package/dist/types/esm/types/AppSettings.types.d.ts +26 -0
  200. package/dist/types/esm/types/AppSettings.types.d.ts.map +1 -0
  201. package/dist/types/esm/types/Config.types.d.ts +12 -0
  202. package/dist/types/esm/types/Config.types.d.ts.map +1 -0
  203. package/dist/types/esm/types/Source.types.d.ts +5 -0
  204. package/dist/types/esm/types/Source.types.d.ts.map +1 -0
  205. package/dist/types/tsconfig.build.tsbuildinfo +1 -0
  206. package/package.json +85 -3
  207. package/rollup.config.mjs +25 -0
  208. package/src/__testUtils__/AppSettingsSchema.d.ts +9 -0
  209. package/src/__testUtils__/components/ErrorBoundary.tsx +25 -0
  210. package/src/cli/command.test.ts +36 -0
  211. package/src/cli/command.ts +33 -0
  212. package/src/cli/handler.test.ts +61 -0
  213. package/src/cli/handler.ts +58 -0
  214. package/src/cli/index.test.ts +45 -0
  215. package/src/cli/index.ts +13 -0
  216. package/src/components/AppSettingsProvider.test.tsx +25 -0
  217. package/src/components/AppSettingsProvider.tsx +18 -0
  218. package/src/components/Setting.test.tsx +59 -0
  219. package/src/components/Setting.tsx +43 -0
  220. package/src/config/AppSettingsImplementation.test.ts +65 -0
  221. package/src/config/AppSettingsImplementation.ts +61 -0
  222. package/src/config/ConfigMerger.test.ts +44 -0
  223. package/src/config/ConfigMerger.ts +9 -0
  224. package/src/config/ConfigReader.test.ts +56 -0
  225. package/src/config/ConfigReader.ts +45 -0
  226. package/src/config/ConfigStore.test.ts +32 -0
  227. package/src/config/ConfigStore.ts +12 -0
  228. package/src/config/ConfigValidator.test.ts +30 -0
  229. package/src/config/ConfigValidator.ts +13 -0
  230. package/src/config/ScopedSettingsImplementation.test.ts +31 -0
  231. package/src/config/ScopedSettingsImplementation.ts +36 -0
  232. package/src/config/createAppSettings.test.ts +84 -0
  233. package/src/config/createAppSettings.ts +18 -0
  234. package/src/config/helpers/freezeDeep.test.ts +41 -0
  235. package/src/config/helpers/freezeDeep.ts +15 -0
  236. package/src/hooks/useAppSetting.test.tsx +37 -0
  237. package/src/hooks/useAppSetting.ts +14 -0
  238. package/src/hooks/useAppSettings.test.tsx +37 -0
  239. package/src/hooks/useAppSettings.ts +17 -0
  240. package/src/hooks/useAppSettingsContext.test.tsx +45 -0
  241. package/src/hooks/useAppSettingsContext.ts +17 -0
  242. package/src/index.ts +11 -0
  243. package/src/server.ts +7 -0
  244. package/src/sources/AppSettingsSource.test.ts +61 -0
  245. package/src/sources/AppSettingsSource.ts +30 -0
  246. package/src/sources/EnvironmentSource.test.ts +45 -0
  247. package/src/sources/EnvironmentSource.ts +87 -0
  248. package/src/sources/MemorySource.test.ts +31 -0
  249. package/src/sources/MemorySource.ts +21 -0
  250. package/src/sources/RemoteSource.test.ts +80 -0
  251. package/src/sources/RemoteSource.ts +50 -0
  252. package/src/sources/helpers/loadJson.test.ts +74 -0
  253. package/src/sources/helpers/loadJson.ts +29 -0
  254. package/src/types/AppSettings.types.test.ts +61 -0
  255. package/src/types/AppSettings.types.ts +30 -0
  256. package/src/types/Config.types.test.ts +36 -0
  257. package/src/types/Config.types.ts +27 -0
  258. package/src/types/Source.types.ts +4 -0
  259. package/tsconfig.build.json +10 -0
  260. package/tsconfig.json +10 -0
package/package.json CHANGED
@@ -1,6 +1,88 @@
1
1
  {
2
2
  "name": "@next-box/appsettings",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "A configuration management library for TypeScript applications, inspired by .NET’s appsettings.json.",
4
+ "version": "0.0.2",
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/index.d.ts",
38
+ "require": "./dist/types/cjs/cli/index.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,25 @@
1
+ import config from '../../rollup.config.mjs';
2
+
3
+ const { input, output, ...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/index'),
12
+ output: {
13
+ ...output,
14
+ dir: `${output.dir}/cli`,
15
+ },
16
+ },
17
+ {
18
+ ...rest,
19
+ input: input.replace('index', 'server'),
20
+ output: {
21
+ ...output,
22
+ dir: `${output.dir}/server`,
23
+ },
24
+ },
25
+ ];
@@ -0,0 +1,9 @@
1
+ declare module '../index.ts' {
2
+ interface AppSettingsSchema {
3
+ Api: { Missing?: string; Timeout: number };
4
+ Features: { DarkMode: boolean };
5
+ Missing: { Value: string };
6
+ }
7
+ }
8
+
9
+ export {};
@@ -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
+ };
@@ -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 './index.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
+ });
@@ -0,0 +1,13 @@
1
+ import yargs from 'yargs';
2
+ import { hideBin } from 'yargs/helpers';
3
+ import * as generateTypesCommand from './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
+ });