@evolu/common 8.9.0 → 8.10.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.
Files changed (233) hide show
  1. package/dist/src/Bytes.d.ts +647 -0
  2. package/dist/src/Bytes.d.ts.map +1 -0
  3. package/dist/src/{Binary.js → Bytes.js} +266 -16
  4. package/dist/src/Config.d.ts +142 -0
  5. package/dist/src/Config.d.ts.map +1 -0
  6. package/dist/src/Config.js +181 -0
  7. package/dist/src/Fs.d.ts +302 -0
  8. package/dist/src/Fs.d.ts.map +1 -0
  9. package/dist/src/Fs.js +111 -0
  10. package/dist/src/Number.d.ts +50 -7
  11. package/dist/src/Number.d.ts.map +1 -1
  12. package/dist/src/Number.js +47 -8
  13. package/dist/src/Object.d.ts +32 -0
  14. package/dist/src/Object.d.ts.map +1 -1
  15. package/dist/src/Object.js +41 -0
  16. package/dist/src/Sqlite.js +1 -1
  17. package/dist/src/Time.d.ts +74 -12
  18. package/dist/src/Time.d.ts.map +1 -1
  19. package/dist/src/Time.js +40 -2
  20. package/dist/src/Type.d.ts +1451 -79
  21. package/dist/src/Type.d.ts.map +1 -1
  22. package/dist/src/Type.js +1303 -96
  23. package/dist/src/index.d.ts +3 -1
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +3 -1
  26. package/dist/src/intl/_en.d.ts +24 -1
  27. package/dist/src/intl/_en.d.ts.map +1 -1
  28. package/dist/src/intl/_en.js +20 -0
  29. package/dist/src/intl/ar.d.ts +24 -1
  30. package/dist/src/intl/ar.d.ts.map +1 -1
  31. package/dist/src/intl/ar.js +20 -0
  32. package/dist/src/intl/bn.d.ts +24 -1
  33. package/dist/src/intl/bn.d.ts.map +1 -1
  34. package/dist/src/intl/bn.js +20 -0
  35. package/dist/src/intl/ca.d.ts +24 -1
  36. package/dist/src/intl/ca.d.ts.map +1 -1
  37. package/dist/src/intl/ca.js +20 -0
  38. package/dist/src/intl/cs.d.ts +24 -1
  39. package/dist/src/intl/cs.d.ts.map +1 -1
  40. package/dist/src/intl/cs.js +20 -0
  41. package/dist/src/intl/da.d.ts +24 -1
  42. package/dist/src/intl/da.d.ts.map +1 -1
  43. package/dist/src/intl/da.js +20 -0
  44. package/dist/src/intl/de.d.ts +24 -1
  45. package/dist/src/intl/de.d.ts.map +1 -1
  46. package/dist/src/intl/de.js +20 -0
  47. package/dist/src/intl/el.d.ts +24 -1
  48. package/dist/src/intl/el.d.ts.map +1 -1
  49. package/dist/src/intl/el.js +20 -0
  50. package/dist/src/intl/es.d.ts +24 -1
  51. package/dist/src/intl/es.d.ts.map +1 -1
  52. package/dist/src/intl/es.js +20 -0
  53. package/dist/src/intl/fa.d.ts +24 -1
  54. package/dist/src/intl/fa.d.ts.map +1 -1
  55. package/dist/src/intl/fa.js +20 -0
  56. package/dist/src/intl/fi.d.ts +24 -1
  57. package/dist/src/intl/fi.d.ts.map +1 -1
  58. package/dist/src/intl/fi.js +20 -0
  59. package/dist/src/intl/fil.d.ts +24 -1
  60. package/dist/src/intl/fil.d.ts.map +1 -1
  61. package/dist/src/intl/fil.js +20 -0
  62. package/dist/src/intl/fr.d.ts +24 -1
  63. package/dist/src/intl/fr.d.ts.map +1 -1
  64. package/dist/src/intl/fr.js +20 -0
  65. package/dist/src/intl/he.d.ts +24 -1
  66. package/dist/src/intl/he.d.ts.map +1 -1
  67. package/dist/src/intl/he.js +20 -0
  68. package/dist/src/intl/hi.d.ts +24 -1
  69. package/dist/src/intl/hi.d.ts.map +1 -1
  70. package/dist/src/intl/hi.js +20 -0
  71. package/dist/src/intl/hr.d.ts +24 -1
  72. package/dist/src/intl/hr.d.ts.map +1 -1
  73. package/dist/src/intl/hr.js +20 -0
  74. package/dist/src/intl/hu.d.ts +22 -1
  75. package/dist/src/intl/hu.d.ts.map +1 -1
  76. package/dist/src/intl/hu.js +18 -0
  77. package/dist/src/intl/id.d.ts +24 -1
  78. package/dist/src/intl/id.d.ts.map +1 -1
  79. package/dist/src/intl/id.js +20 -0
  80. package/dist/src/intl/it.d.ts +24 -1
  81. package/dist/src/intl/it.d.ts.map +1 -1
  82. package/dist/src/intl/it.js +20 -0
  83. package/dist/src/intl/ja.d.ts +24 -1
  84. package/dist/src/intl/ja.d.ts.map +1 -1
  85. package/dist/src/intl/ja.js +20 -0
  86. package/dist/src/intl/ko.d.ts +24 -1
  87. package/dist/src/intl/ko.d.ts.map +1 -1
  88. package/dist/src/intl/ko.js +20 -0
  89. package/dist/src/intl/ml.d.ts +24 -1
  90. package/dist/src/intl/ml.d.ts.map +1 -1
  91. package/dist/src/intl/ml.js +20 -0
  92. package/dist/src/intl/mr.d.ts +24 -1
  93. package/dist/src/intl/mr.d.ts.map +1 -1
  94. package/dist/src/intl/mr.js +20 -0
  95. package/dist/src/intl/ms.d.ts +24 -1
  96. package/dist/src/intl/ms.d.ts.map +1 -1
  97. package/dist/src/intl/ms.js +20 -0
  98. package/dist/src/intl/nb.d.ts +22 -1
  99. package/dist/src/intl/nb.d.ts.map +1 -1
  100. package/dist/src/intl/nb.js +18 -0
  101. package/dist/src/intl/nl.d.ts +24 -1
  102. package/dist/src/intl/nl.d.ts.map +1 -1
  103. package/dist/src/intl/nl.js +20 -0
  104. package/dist/src/intl/pa.d.ts +24 -1
  105. package/dist/src/intl/pa.d.ts.map +1 -1
  106. package/dist/src/intl/pa.js +20 -0
  107. package/dist/src/intl/pl.d.ts +23 -0
  108. package/dist/src/intl/pl.d.ts.map +1 -1
  109. package/dist/src/intl/pl.js +20 -0
  110. package/dist/src/intl/pt-BR.d.ts +24 -1
  111. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  112. package/dist/src/intl/pt-BR.js +20 -0
  113. package/dist/src/intl/pt.d.ts +24 -1
  114. package/dist/src/intl/pt.d.ts.map +1 -1
  115. package/dist/src/intl/pt.js +20 -0
  116. package/dist/src/intl/ro.d.ts +24 -1
  117. package/dist/src/intl/ro.d.ts.map +1 -1
  118. package/dist/src/intl/ro.js +20 -0
  119. package/dist/src/intl/sk.d.ts +24 -1
  120. package/dist/src/intl/sk.d.ts.map +1 -1
  121. package/dist/src/intl/sk.js +20 -0
  122. package/dist/src/intl/sl.d.ts +24 -1
  123. package/dist/src/intl/sl.d.ts.map +1 -1
  124. package/dist/src/intl/sl.js +20 -0
  125. package/dist/src/intl/sv.d.ts +24 -1
  126. package/dist/src/intl/sv.d.ts.map +1 -1
  127. package/dist/src/intl/sv.js +20 -0
  128. package/dist/src/intl/sw.d.ts +21 -0
  129. package/dist/src/intl/sw.d.ts.map +1 -1
  130. package/dist/src/intl/sw.js +18 -0
  131. package/dist/src/intl/ta.d.ts +24 -1
  132. package/dist/src/intl/ta.d.ts.map +1 -1
  133. package/dist/src/intl/ta.js +20 -0
  134. package/dist/src/intl/te.d.ts +24 -1
  135. package/dist/src/intl/te.d.ts.map +1 -1
  136. package/dist/src/intl/te.js +20 -0
  137. package/dist/src/intl/th.d.ts +24 -1
  138. package/dist/src/intl/th.d.ts.map +1 -1
  139. package/dist/src/intl/th.js +20 -0
  140. package/dist/src/intl/tr.d.ts +24 -1
  141. package/dist/src/intl/tr.d.ts.map +1 -1
  142. package/dist/src/intl/tr.js +20 -0
  143. package/dist/src/intl/uk.d.ts +80 -57
  144. package/dist/src/intl/uk.d.ts.map +1 -1
  145. package/dist/src/intl/uk.js +174 -149
  146. package/dist/src/intl/ur.d.ts +24 -1
  147. package/dist/src/intl/ur.d.ts.map +1 -1
  148. package/dist/src/intl/ur.js +20 -0
  149. package/dist/src/intl/vi.d.ts +24 -1
  150. package/dist/src/intl/vi.d.ts.map +1 -1
  151. package/dist/src/intl/vi.js +20 -0
  152. package/dist/src/intl/zh-CN.d.ts +24 -1
  153. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  154. package/dist/src/intl/zh-CN.js +20 -0
  155. package/dist/src/intl/zh-TW.d.ts +24 -1
  156. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  157. package/dist/src/intl/zh-TW.js +20 -0
  158. package/dist/src/local-first/Protocol.d.ts +1 -1
  159. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  160. package/dist/src/local-first/Protocol.js +1 -1
  161. package/dist/src/local-first/Schema.d.ts +10 -0
  162. package/dist/src/local-first/Schema.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +1 -1
  164. package/dist/src/local-first/Timestamp.js +1 -1
  165. package/package.json +1 -1
  166. package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
  167. package/src/{Binary.ts → Bytes.ts} +652 -21
  168. package/src/Config.test.ts +672 -0
  169. package/src/Config.ts +410 -0
  170. package/src/Fs.test.ts +105 -0
  171. package/src/Fs.ts +414 -0
  172. package/src/Number.test.ts +82 -18
  173. package/src/Number.ts +76 -8
  174. package/src/Object.test.ts +115 -1
  175. package/src/Object.ts +44 -0
  176. package/src/Sqlite.ts +1 -1
  177. package/src/Time.test.ts +82 -11
  178. package/src/Time.ts +141 -16
  179. package/src/Type.test.ts +3114 -157
  180. package/src/Type.ts +2553 -196
  181. package/src/index.ts +3 -1
  182. package/src/intl/_en.ts +70 -0
  183. package/src/intl/ar.ts +71 -0
  184. package/src/intl/bn.ts +70 -0
  185. package/src/intl/ca.ts +70 -0
  186. package/src/intl/cs.ts +70 -0
  187. package/src/intl/da.ts +70 -0
  188. package/src/intl/de.ts +70 -0
  189. package/src/intl/el.ts +70 -0
  190. package/src/intl/es.ts +70 -0
  191. package/src/intl/fa.ts +70 -0
  192. package/src/intl/fi.ts +70 -0
  193. package/src/intl/fil.ts +70 -0
  194. package/src/intl/fr.ts +70 -0
  195. package/src/intl/he.ts +70 -0
  196. package/src/intl/hi.ts +70 -0
  197. package/src/intl/hr.ts +70 -0
  198. package/src/intl/hu.ts +69 -0
  199. package/src/intl/id.ts +70 -0
  200. package/src/intl/intl.test.ts +819 -1
  201. package/src/intl/it.ts +70 -0
  202. package/src/intl/ja.ts +70 -0
  203. package/src/intl/ko.ts +68 -0
  204. package/src/intl/ml.ts +70 -0
  205. package/src/intl/mr.ts +70 -0
  206. package/src/intl/ms.ts +71 -0
  207. package/src/intl/nb.ts +69 -0
  208. package/src/intl/nl.ts +70 -0
  209. package/src/intl/pa.ts +70 -0
  210. package/src/intl/pl.ts +63 -0
  211. package/src/intl/pt-BR.ts +70 -0
  212. package/src/intl/pt.ts +71 -0
  213. package/src/intl/ro.ts +70 -0
  214. package/src/intl/sk.ts +71 -0
  215. package/src/intl/sl.ts +70 -0
  216. package/src/intl/sv.ts +70 -0
  217. package/src/intl/sw.ts +62 -0
  218. package/src/intl/ta.ts +70 -0
  219. package/src/intl/te.ts +70 -0
  220. package/src/intl/th.ts +68 -0
  221. package/src/intl/tr.ts +70 -0
  222. package/src/intl/uk.ts +228 -155
  223. package/src/intl/ur.ts +70 -0
  224. package/src/intl/vi.ts +70 -0
  225. package/src/intl/zh-CN.ts +68 -0
  226. package/src/intl/zh-TW.ts +68 -0
  227. package/src/local-first/Protocol.test.ts +1 -1
  228. package/src/local-first/Protocol.ts +1 -1
  229. package/src/local-first/Schema.ts +11 -0
  230. package/src/local-first/Storage.ts +1 -1
  231. package/src/local-first/Timestamp.ts +1 -1
  232. package/dist/src/Binary.d.ts +0 -254
  233. package/dist/src/Binary.d.ts.map +0 -1
@@ -0,0 +1,672 @@
1
+ import nodeAssert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ assertEqual,
5
+ assertErr,
6
+ assertFalse,
7
+ assertOk,
8
+ assertTrue,
9
+ } from "./Assert.ts";
10
+ import { ByteLengthFromString, byteSizeToByteLength } from "./Bytes.ts";
11
+ import { EnvName, env, type EnvType } from "./Config.ts";
12
+ import {
13
+ array,
14
+ assertType,
15
+ BooleanFromString,
16
+ Int,
17
+ json,
18
+ localizeTypes,
19
+ Number,
20
+ object,
21
+ optional,
22
+ Port,
23
+ PortFromString,
24
+ String,
25
+ typeErrorToIssues,
26
+ withDefault,
27
+ type Type,
28
+ type TypeError,
29
+ type TypeNode,
30
+ } from "./Type.ts";
31
+ import * as cs from "./intl/cs.ts";
32
+
33
+ describe("EnvName", () => {
34
+ it("bounds canonical names including the prefix", () => {
35
+ assertOk(EnvName.fromUnknown("A".repeat(255)));
36
+ for (const value of [
37
+ "A".repeat(256),
38
+ "",
39
+ "APP_port",
40
+ "APP__PORT",
41
+ "APP_PORT_",
42
+ "APP_2FA",
43
+ "APP-PΟRT",
44
+ ]) {
45
+ assertErr(EnvName.fromUnknown(value));
46
+ }
47
+ });
48
+ });
49
+
50
+ describe("env", () => {
51
+ it("combines unprefixed fields and multiple namespaces into a flat output", () => {
52
+ const Env = env({
53
+ port: withDefault(optional(PortFromString), Port.orThrow(4000)),
54
+ EVOLU_RELAY: {
55
+ maxOwnerBytes: withDefault(
56
+ optional(ByteLengthFromString),
57
+ byteSizeToByteLength("1MiB"),
58
+ ),
59
+ },
60
+ AUTH: { token: String },
61
+ });
62
+ const result = Env.fromUnknown({
63
+ PORT: "04001",
64
+ EVOLU_RELAY_MAX_OWNER_BYTES: "512KiB",
65
+ AUTH_TOKEN: "token",
66
+ PATH: "/bin",
67
+ HOME: "/home",
68
+ NEXT_OTHER: "ignored",
69
+ });
70
+ assertOk(result, { port: 4001, maxOwnerBytes: 524288, token: "token" });
71
+ assertType<typeof result.value.port, Port>();
72
+ assertType<typeof result.value.token, string>();
73
+ assertEqual(Env.to(result.value), {
74
+ PORT: "4001",
75
+ EVOLU_RELAY_MAX_OWNER_BYTES: "524288",
76
+ AUTH_TOKEN: "token",
77
+ });
78
+ assertOk(Env.fromUnknown(Env.to(result.value)), result.value);
79
+ assertOk(Env.fromUnknown({ AUTH_TOKEN: "token" }), {
80
+ port: 4000,
81
+ maxOwnerBytes: 1048576,
82
+ token: "token",
83
+ });
84
+ const invalid = Env.fromUnknown(
85
+ { PORT: "bad", EVOLU_RELAY_MAX_OWNER_BYTES: "bad", AUTH_TOKNE: "bad" },
86
+ { errors: "all" },
87
+ );
88
+ assertErr(invalid);
89
+ assertEqual(
90
+ typeErrorToIssues(Env, invalid.error).map(({ path }) => path),
91
+ [
92
+ ["PORT"],
93
+ ["EVOLU_RELAY_MAX_OWNER_BYTES"],
94
+ ["AUTH_TOKEN"],
95
+ ["AUTH_TOKNE"],
96
+ ],
97
+ );
98
+ });
99
+
100
+ it("selects exact unprefixed names and rejects unknown namespace names regardless of casing", () => {
101
+ const Env = env({
102
+ port: optional(PortFromString),
103
+ APP: { name: optional(String) },
104
+ AUTH: {},
105
+ });
106
+ assertOk(
107
+ Env.fromUnknown({
108
+ PATH: "/bin",
109
+ HOME: "/home",
110
+ port: "bad",
111
+ POTR: "bad",
112
+ APPLICATION_NAME: "ignored",
113
+ }),
114
+ {},
115
+ );
116
+ for (const name of [
117
+ "APP_UNKNOWN",
118
+ "app_NAME",
119
+ "App_name",
120
+ "APP_name",
121
+ "AUTH_UNKNOWN",
122
+ "auth_UNKNOWN",
123
+ ]) {
124
+ const result = Env.fromUnknown({ [name]: "value" });
125
+ assertErr(result);
126
+ assertEqual(typeErrorToIssues(Env, result.error)[0]?.path, [name]);
127
+ }
128
+ assertOk(env({}).fromUnknown(process.env), {});
129
+ assertEqual(env({}).to({}), {});
130
+ });
131
+
132
+ it("rejects duplicate output fields and encoded names", () => {
133
+ nodeAssert.throws(
134
+ () => env({ port: String, APP: { port: String } }),
135
+ /Duplicate environment field "port"/u,
136
+ );
137
+ nodeAssert.throws(
138
+ () => env({ APP: { port: String }, AUTH: { port: String } }),
139
+ /Duplicate environment field "port"/u,
140
+ );
141
+ nodeAssert.throws(
142
+ () => env({ appPort: String, APP: { port: String } }),
143
+ /Duplicate environment variable "APP_PORT"/u,
144
+ );
145
+ nodeAssert.throws(
146
+ () => env({ APP: { authToken: String }, APP_AUTH: { token: String } }),
147
+ /Duplicate environment variable "APP_AUTH_TOKEN"/u,
148
+ );
149
+ });
150
+
151
+ it("allows overlapping namespaces with unambiguous declared names", () => {
152
+ const Env = env({ APP: { name: String }, APP_AUTH: { token: String } });
153
+ assertOk(Env.fromUnknown({ APP_NAME: "app", APP_AUTH_TOKEN: "token" }), {
154
+ name: "app",
155
+ token: "token",
156
+ });
157
+ const result = Env.fromUnknown({
158
+ APP_NAME: "app",
159
+ APP_AUTH_TOKEN: "token",
160
+ APP_AUTH_UNKNOWN: "bad",
161
+ });
162
+ assertErr(result);
163
+ assertEqual(typeErrorToIssues(Env, result.error)[0]?.path, [
164
+ "APP_AUTH_UNKNOWN",
165
+ ]);
166
+ });
167
+
168
+ it("snapshots declarations and never invokes schema getters", () => {
169
+ const group = { name: String };
170
+ const props = { port: optional(PortFromString), APP: group };
171
+ const Env = env(props);
172
+ Object.assign(group, { name: Number, added: String });
173
+ Object.assign(props, { port: Number, OTHER: { extra: String } });
174
+ assertOk(Env.fromUnknown({ APP_NAME: "value", PORT: "4000" }), {
175
+ port: 4000,
176
+ name: "value",
177
+ });
178
+ assertErr(Env.fromUnknown({ APP_ADDED: "value" }));
179
+ let reads = 0;
180
+ const accessor = {
181
+ enumerable: true,
182
+ get: () => {
183
+ reads++;
184
+ return String;
185
+ },
186
+ };
187
+ nodeAssert.throws(
188
+ () => env(Object.defineProperty({}, "port", accessor)),
189
+ /Environment properties must be enumerable string data properties/u,
190
+ );
191
+ nodeAssert.throws(
192
+ () => env({ APP: Object.defineProperty({}, "name", accessor) }),
193
+ /Environment fields must be enumerable string data properties/u,
194
+ );
195
+ nodeAssert.throws(
196
+ () => env(Object.defineProperty({}, "port", { value: String })),
197
+ /Environment properties must be enumerable string data properties/u,
198
+ );
199
+ nodeAssert.throws(
200
+ () => env({ APP: Object.defineProperty({}, "name", { value: String }) }),
201
+ /Environment fields must be enumerable string data properties/u,
202
+ );
203
+ nodeAssert.throws(
204
+ // @ts-expect-error Environment properties must use fixed string keys.
205
+ () => env(Object.assign({}, { [Symbol("invalidKey")]: String })),
206
+ /Environment properties must be enumerable string data properties/u,
207
+ );
208
+ nodeAssert.throws(
209
+ // @ts-expect-error Environment properties must use fixed string keys.
210
+ () => env({ APP: Object.assign({}, { [Symbol("invalidKey")]: String }) }),
211
+ /Environment fields must be enumerable string data properties/u,
212
+ );
213
+ assertEqual(reads, 0);
214
+ nodeAssert.throws(
215
+ () => env(Object.create({ port: String }) as { port: typeof String }),
216
+ /Environment properties must be a plain object/u,
217
+ );
218
+ nodeAssert.throws(
219
+ () =>
220
+ env({
221
+ APP: Object.create({ port: String }) as { port: typeof String },
222
+ }),
223
+ /Environment groups must be plain objects/u,
224
+ );
225
+ nodeAssert.throws(() => {
226
+ // @ts-expect-error Namespace groups contain fields, not nested groups.
227
+ env({ APP: { INNER: { name: String } } });
228
+ }, /groups cannot be nested/u);
229
+ });
230
+
231
+ it("supports prototype-like field names without prototype mutation", () => {
232
+ const Env = env({ APP: { constructor: String, toString: String } });
233
+ const result = Env.fromUnknown({
234
+ APP_CONSTRUCTOR: "value",
235
+ APP_TO_STRING: "text",
236
+ });
237
+ assertOk(result, { constructor: "value", toString: "text" });
238
+ assertEqual(Env.to(result.value), {
239
+ APP_CONSTRUCTOR: "value",
240
+ APP_TO_STRING: "text",
241
+ });
242
+ });
243
+
244
+ it("selects variables, decodes strict values, and canonically encodes them", () => {
245
+ const Settings = object({
246
+ port: PortFromString,
247
+ maxOwnerBytes: optional(ByteLengthFromString),
248
+ verbose: optional(BooleanFromString),
249
+ });
250
+ const Env = env({ APP: Settings.props });
251
+ assertType<typeof Env, EnvType<{ readonly APP: typeof Settings.props }>>();
252
+ const result = Env.fromUnknown({
253
+ APP_PORT: "04000",
254
+ APP_MAX_OWNER_BYTES: "1MiB",
255
+ APP_VERBOSE: "false",
256
+ HOME: "/home/evolu",
257
+ other: 42,
258
+ });
259
+ assertOk(result, { port: 4000, maxOwnerBytes: 1048576, verbose: false });
260
+ assertType<typeof result.value, typeof Settings.Output>();
261
+ assertEqual(Env.to(result.value), {
262
+ APP_PORT: "4000",
263
+ APP_MAX_OWNER_BYTES: "1048576",
264
+ APP_VERBOSE: "false",
265
+ });
266
+ assertOk(Env.fromUnknown(Env.to(result.value)), result.value);
267
+ assertOk(Env.from.parent({ APP_PORT: "4000" }), { port: 4000 });
268
+ assertOk(Env.from(result.value), result.value);
269
+ assertTrue(Env.is(result.value));
270
+ const other = Env.output.key.orThrow("other");
271
+ assertOk(Env.output.key.fromUnknown(Env.output.key.to(other)), other);
272
+ assertFalse(Env.is({ port: "4000" }));
273
+ assertFalse(Env.is({ port: 65536 }));
274
+ assertEqual(Env.orNull({ APP_PORT: "invalid" }), null);
275
+ nodeAssert.throws(() => Env.to({ port: 65536 as Port }));
276
+ assertEqual(
277
+ Env.orThrow({ APP_PORT: "4000", APP_MAX_OWNER_BYTES: "1000001" })
278
+ .maxOwnerBytes,
279
+ 1000001,
280
+ );
281
+ assertEqual(
282
+ Env.orThrow({ APP_PORT: "4000" }).maxOwnerBytes ??
283
+ byteSizeToByteLength("1MiB"),
284
+ 1048576,
285
+ );
286
+ });
287
+
288
+ it("preserves empty strings and rejects invalid or unknown selected values", () => {
289
+ const Env = env({
290
+ APP: { port: optional(PortFromString), name: optional(String) },
291
+ });
292
+ assertOk(Env.fromUnknown({ APP_NAME: "" }), { name: "" });
293
+ assertEqual(Env.to(Env.orThrow({})), {});
294
+ for (const source of [
295
+ { APP_PORT: "" },
296
+ { APP_PORT: " " },
297
+ { APP_PORT: "65536" },
298
+ { APP_PORT: "-1" },
299
+ { APP_PORT: 4000 },
300
+ { APP_PORT: null },
301
+ { APP_POTR: "4000" },
302
+ { APP_port: "4000" },
303
+ { app_PORT: "4000" },
304
+ { app_port: "4000" },
305
+ { APP__PORT: "4000" },
306
+ { APP_PORT_: "4000" },
307
+ { APP_DB__PORT: "4000" },
308
+ { APP_: "4000" },
309
+ { ["APP_" + "A".repeat(252)]: "4000" },
310
+ ])
311
+ assertErr(Env.fromUnknown(source));
312
+ assertOk(
313
+ Env.fromUnknown({
314
+ APPPORT: "invalid",
315
+ APPOTHER_PORT: "invalid",
316
+ }),
317
+ {},
318
+ );
319
+ });
320
+
321
+ it("rejects explicit undefined for required, optional, and unknown fields", () => {
322
+ const Env = env({ APP: { port: PortFromString, name: optional(String) } });
323
+ assertOk(Env.fromUnknown({ APP_PORT: "4000", HOME: undefined }), {
324
+ port: 4000,
325
+ });
326
+ for (const key of ["APP_PORT", "APP_NAME", "APP_UNKNOWN"]) {
327
+ const result = Env.fromUnknown({ APP_PORT: "4000", [key]: undefined });
328
+ assertErr(result);
329
+ assertEqual(
330
+ typeErrorToIssues(Env, result.error).map(({ path }) => path),
331
+ [[key]],
332
+ );
333
+ }
334
+ });
335
+
336
+ it("keeps original names in required, invalid, and excess-property issues", () => {
337
+ const Env = env({
338
+ APP: { port: PortFromString, verbose: optional(BooleanFromString) },
339
+ });
340
+ const result = Env.fromUnknown(
341
+ { APP_PORT: "65536", APP_VERBOSE: "maybe", APP_POTR: "4000" },
342
+ { errors: "all" },
343
+ );
344
+ assertErr(result);
345
+ assertEqual(
346
+ typeErrorToIssues(Env, result.error).map(({ path }) => path),
347
+ [["APP_PORT"], ["APP_VERBOSE"], ["APP_POTR"]],
348
+ );
349
+ assertEqual(
350
+ typeErrorToIssues(Env, result.error)[0]?.message,
351
+ "The value 65536 must be less than or equal to 65535.",
352
+ );
353
+ const missing = Env.fromUnknown({});
354
+ assertErr(missing);
355
+ assertEqual(typeErrorToIssues(Env, missing.error), [
356
+ {
357
+ path: ["APP_PORT"],
358
+ message: 'The required property "APP_PORT" is missing.',
359
+ },
360
+ ]);
361
+ const first = Env.fromUnknown({
362
+ APP_PORT: "invalid",
363
+ APP_VERBOSE: "maybe",
364
+ });
365
+ assertErr(first);
366
+ assertEqual(typeErrorToIssues(Env, first.error).length, 1);
367
+ assertEqual(
368
+ Env["~standard"].validate({ APP_PORT: "invalid", APP_VERBOSE: "maybe" }),
369
+ {
370
+ issues: [
371
+ {
372
+ path: ["APP_PORT"],
373
+ message: 'The value "invalid" is not a decimal integer.',
374
+ },
375
+ {
376
+ path: ["APP_VERBOSE"],
377
+ message: 'The value "maybe" is not a boolean. Use true or false.',
378
+ },
379
+ ],
380
+ },
381
+ );
382
+ });
383
+
384
+ it("preserves nested JSON paths without implicit ENV nesting", () => {
385
+ const [PortsJson] = json(array(Int), "PortsJson");
386
+ const Env = env({ APP: { ports: PortsJson } });
387
+ const result = Env.fromUnknown({ APP_PORTS: '[80,"invalid"]' });
388
+ assertErr(result);
389
+ assertEqual(typeErrorToIssues(Env, result.error)[0]?.path, [
390
+ "APP_PORTS",
391
+ 1,
392
+ ]);
393
+ assertEqual(Env.to(Env.orThrow({ APP_PORTS: "[80,443]" })), {
394
+ APP_PORTS: "[80,443]",
395
+ });
396
+ });
397
+
398
+ it("composes explicit defaults without losing supplied-input evidence", () => {
399
+ const Env = env({
400
+ APP: {
401
+ port: withDefault(optional(PortFromString), Port.orThrow(4000), {
402
+ strategy: "preserve",
403
+ }),
404
+ },
405
+ });
406
+ const missing = Env.fromUnknown({});
407
+ assertOk(missing, {
408
+ port: { value: 4000, defaultUsed: true, original: "missing" },
409
+ });
410
+ assertEqual(Env.to(missing.value), {});
411
+ const supplied = Env.fromUnknown({ APP_PORT: "04000" });
412
+ assertOk(supplied, { port: { value: 4000, defaultUsed: false } });
413
+ assertEqual(Env.to(supplied.value), { APP_PORT: "4000" });
414
+ const invalid = Env.fromUnknown({ APP_PORT: "65536" });
415
+ assertErr(invalid);
416
+ assertEqual(typeErrorToIssues(Env, invalid.error), [
417
+ {
418
+ path: ["APP_PORT"],
419
+ message: "The value 65536 must be less than or equal to 65535.",
420
+ },
421
+ ]);
422
+ assertErr(Env.fromUnknown({ APP_PORT: undefined }));
423
+ });
424
+
425
+ it("supports unprefixed fields, host objects, and descriptor-safe selection", () => {
426
+ const Env = env({ port: PortFromString });
427
+ assertOk(Env.fromUnknown({ PORT: "4000" }), { port: 4000 });
428
+ assertEqual(Env.to(Env.orThrow({ PORT: "04000" })), { PORT: "4000" });
429
+ const AppEnv = env({ APP: { port: optional(PortFromString) } });
430
+ assertOk(AppEnv.fromUnknown(Object.create({ APP_PORT: "invalid" })), {});
431
+ assertOk(
432
+ AppEnv.fromUnknown(
433
+ Object.assign(Object.create(null), { APP_PORT: "4000" }),
434
+ ),
435
+ { port: 4000 },
436
+ );
437
+ let reads = 0;
438
+ const source = Object.defineProperties(
439
+ {},
440
+ {
441
+ APP_PORT: {
442
+ enumerable: true,
443
+ get: () => {
444
+ reads++;
445
+ return "4000";
446
+ },
447
+ },
448
+ HOME: {
449
+ enumerable: true,
450
+ get: () => {
451
+ reads++;
452
+ return "ignored";
453
+ },
454
+ },
455
+ },
456
+ );
457
+ assertErr(AppEnv.fromUnknown(source));
458
+ assertEqual(reads, 0);
459
+ assertErr(
460
+ AppEnv.fromUnknown(
461
+ Object.defineProperty({}, "APP_PORT", { value: undefined }),
462
+ ),
463
+ );
464
+ assertOk(
465
+ AppEnv.fromUnknown(
466
+ Object.defineProperty({}, "HOME", {
467
+ get: () => {
468
+ throw new Error("Unrelated getter");
469
+ },
470
+ }),
471
+ ),
472
+ {},
473
+ );
474
+ assertOk(AppEnv.fromUnknown({ [Symbol("APP_PORT")]: "ignored" }), {});
475
+ assertOk(env({ EVOLU_CONFIG_TEST: {} }).fromUnknown(process.env), {});
476
+ for (const value of [null, undefined, "", 42, false]) {
477
+ const result = AppEnv.fromUnknown(value);
478
+ assertErr(result, {
479
+ type: "Env",
480
+ outputError: {
481
+ type: "ObjectKeys",
482
+ error: { type: "Object", reason: { kind: "NotObject", value } },
483
+ },
484
+ });
485
+ assertEqual(typeErrorToIssues(AppEnv, result.error), [
486
+ {
487
+ path: [],
488
+ message: `A value ${JSON.stringify(value)} is not an object.`,
489
+ },
490
+ ]);
491
+ }
492
+ });
493
+
494
+ it("selects own properties consistently from arrays and other objects", () => {
495
+ const Env = env({ APP: { port: optional(PortFromString) } });
496
+ const Required = env({ APP: { port: PortFromString } });
497
+ for (const source of [
498
+ [],
499
+ new Date(0),
500
+ new Map([["APP_PORT", "4000"]]),
501
+ /value/u,
502
+ ]) {
503
+ assertOk(Env.fromUnknown(source), {});
504
+ const missing = Required.fromUnknown(source);
505
+ assertErr(missing);
506
+ assertEqual(typeErrorToIssues(Required, missing.error)[0]?.path, [
507
+ "APP_PORT",
508
+ ]);
509
+
510
+ Object.assign(source, { APP_PORT: "4000" });
511
+ assertOk(Required.fromUnknown(source), { port: 4000 });
512
+ Object.assign(source, { APP_PORT: "invalid" });
513
+ const invalid = Env.fromUnknown(source);
514
+ assertErr(invalid);
515
+ assertEqual(typeErrorToIssues(Env, invalid.error)[0]?.path, ["APP_PORT"]);
516
+ }
517
+
518
+ const Unprefixed = env({ port: optional(PortFromString) });
519
+ const result = Unprefixed.fromUnknown([]);
520
+ assertOk(result, {});
521
+ });
522
+
523
+ it("rejects invalid schema names at construction", () => {
524
+ nodeAssert.throws(
525
+ () => {
526
+ // @ts-expect-error Environment fields must use camelCase names.
527
+ env({ APP: { Port: PortFromString } });
528
+ },
529
+ {
530
+ message:
531
+ 'Invalid environment field "Port". Expected a camelCase identifier; groups cannot be nested.',
532
+ },
533
+ );
534
+ nodeAssert.throws(() =>
535
+ // @ts-expect-error Environment properties must use fixed string keys.
536
+ env({ APP: { ["a".repeat(252)]: String } }),
537
+ );
538
+ // @ts-expect-error Environment properties must use fixed string keys.
539
+ const Env = env({ APP: { ["a".repeat(251)]: String } });
540
+ assertOk(Env.fromUnknown({ ["APP_" + "A".repeat(251)]: "value" }));
541
+ });
542
+
543
+ it("rejects malformed namespace declarations", () => {
544
+ for (const name of ["APP_", "APP__", "APP-NAME", "", "Port"]) {
545
+ nodeAssert.throws(
546
+ // @ts-expect-error Environment properties must use fixed string keys.
547
+ () => env({ [name]: {} }),
548
+ /Invalid environment schema name/u,
549
+ );
550
+ }
551
+ nodeAssert.throws(
552
+ // @ts-expect-error Environment properties must use fixed string keys.
553
+ () => env({ ["A".repeat(254)]: {} }),
554
+ /Invalid environment namespace/u,
555
+ );
556
+ });
557
+
558
+ it("rejects invalid field and group values with actionable errors", () => {
559
+ const Group = env({ APP: { type: String } });
560
+ assertOk(Group.fromUnknown({ APP_TYPE: "value" }), { type: "value" });
561
+ assertEqual(Group.to({ type: "value" }), { APP_TYPE: "value" });
562
+ nodeAssert.throws(
563
+ // @ts-expect-error Environment namespaces must contain a group of fields.
564
+ () => env({ APP: String }),
565
+ /Environment groups must be plain objects containing fields/u,
566
+ );
567
+ nodeAssert.throws(
568
+ // @ts-expect-error Environment fields must be Types, optional properties, or defaulted properties.
569
+ () => env({ app: { port: String } }),
570
+ /groups must use CONSTANT_CASE names/u,
571
+ );
572
+ nodeAssert.throws(() => {
573
+ // @ts-expect-error Environment declarations require Types or namespace groups.
574
+ env({ port: 4000 });
575
+ }, /Expected a Type or optional\/defaulted property/u);
576
+ });
577
+
578
+ it("localizes field errors through the composed codec", () => {
579
+ const Env = env({ APP: { name: String } });
580
+ const localized = localizeTypes(
581
+ { Env },
582
+ {
583
+ cs: {
584
+ Object: cs.formatObjectError,
585
+ String: cs.formatStringError,
586
+ CamelCaseIdentifier: cs.formatIdentifierError,
587
+ },
588
+ },
589
+ );
590
+ const result = localized.cs.Env.fromUnknown({ APP_NAME: 1 });
591
+ assertErr(result);
592
+ assertEqual(typeErrorToIssues(localized.cs.Env, result.error), [
593
+ { path: ["APP_NAME"], message: "Hodnota 1 musí být text." },
594
+ ]);
595
+ const root = localized.cs.Env.fromUnknown(null);
596
+ const objectRoot = object({}).fromUnknown(null);
597
+ assertErr(root);
598
+ assertErr(objectRoot);
599
+ assertEqual(typeErrorToIssues(localized.cs.Env, root.error), [
600
+ { path: [], message: cs.formatObjectError(objectRoot.error) },
601
+ ]);
602
+ });
603
+
604
+ it("requires concrete field Types that encode to strings", () => {
605
+ const reject = (
606
+ erased: TypeNode,
607
+ reserved: Type<
608
+ "CustomEnv",
609
+ string,
610
+ string,
611
+ TypeError<"ObjectPropertyAccess">
612
+ >,
613
+ broad: Type<"CustomEnv", string, string, TypeError>,
614
+ ) => {
615
+ // @ts-expect-error Environment namespaces must contain a group of fields.
616
+ env({ APP: String });
617
+ // @ts-expect-error Environment namespaces must contain a group of fields.
618
+ env({ APP: optional(String) });
619
+ // @ts-expect-error Environment namespaces must contain a group of fields.
620
+ env({ APP: withDefault(optional(String), "value") });
621
+ // @ts-expect-error Environment fields must be Types, optional properties, or defaulted properties.
622
+ env({ app: { port: String } });
623
+ // @ts-expect-error Environment declarations must use camelCase field names or CONSTANT_CASE namespace names.
624
+ env({ Port: String });
625
+ // @ts-expect-error Environment fields must be Types, optional properties, or defaulted properties.
626
+ env({ port: { type: String } });
627
+ // @ts-expect-error Environment fields must use one concrete Type.
628
+ env({ field: erased });
629
+ // @ts-expect-error Environment fields must not use error tags reserved for Object structure.
630
+ env({ APP: { field: reserved } });
631
+ // @ts-expect-error Environment fields must not use error tags reserved for Object structure.
632
+ env({ field: broad });
633
+ // @ts-expect-error Environment fields must encode to strings.
634
+ env({ port: Number });
635
+ // @ts-expect-error Environment fields must encode to strings.
636
+ env({ APP: { db: object({ port: PortFromString }) } });
637
+ // @ts-expect-error Namespace groups contain fields, not nested groups.
638
+ env({ APP: { NESTED: { port: PortFromString } } });
639
+ // @ts-expect-error env accepts field and namespace declarations, not a prefix string.
640
+ env("APP");
641
+ // @ts-expect-error env accepts field and namespace declarations, not a constructed Type.
642
+ env(object({ port: PortFromString }));
643
+ const uncertain = (true as boolean)
644
+ ? { port: PortFromString }
645
+ : { name: String };
646
+ // @ts-expect-error Environment properties must use one concrete schema.
647
+ env(uncertain);
648
+ // @ts-expect-error Environment properties must use one concrete schema.
649
+ env({ APP: uncertain });
650
+ const field = (true as boolean) ? PortFromString : String;
651
+ // @ts-expect-error Environment fields must use one concrete Type.
652
+ env({ port: field });
653
+ const dynamic: Record<string, typeof String> = {};
654
+ // @ts-expect-error Environment properties must use fixed string keys.
655
+ env(dynamic);
656
+ // @ts-expect-error Environment properties must use fixed string keys.
657
+ env({ APP: dynamic });
658
+ const template = {} as Record<
659
+ `APP_${string}`,
660
+ { port: typeof PortFromString }
661
+ >;
662
+ // @ts-expect-error Environment properties must use fixed string keys.
663
+ env(template);
664
+ // @ts-expect-error Environment properties must use fixed string keys.
665
+ env({ [Symbol("invalidKey")]: String });
666
+ };
667
+ assertType<
668
+ typeof reject extends (...args: Array<never>) => void ? true : false,
669
+ true
670
+ >();
671
+ });
672
+ });