gruber 0.7.0 → 0.9.0-beta.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/CHANGELOG.md +58 -0
  2. package/README.md +82 -8
  3. package/config/README.md +10 -0
  4. package/config/configuration.d.ts +33 -0
  5. package/config/configuration.d.ts.map +1 -0
  6. package/config/configuration.js +181 -0
  7. package/{core → config}/configuration.test.js +299 -176
  8. package/config/configuration.ts +275 -0
  9. package/config/mod.d.ts +7 -0
  10. package/config/mod.d.ts.map +1 -0
  11. package/config/mod.js +6 -0
  12. package/config/mod.ts +6 -0
  13. package/config/parsers.d.ts +20 -0
  14. package/config/parsers.d.ts.map +1 -0
  15. package/config/parsers.js +56 -0
  16. package/config/parsers.test.js +126 -0
  17. package/config/parsers.ts +74 -0
  18. package/config/specifications.d.ts +27 -0
  19. package/config/specifications.d.ts.map +1 -0
  20. package/config/specifications.js +46 -0
  21. package/config/specifications.ts +73 -0
  22. package/config/standard-schema.d.ts +56 -0
  23. package/config/standard-schema.d.ts.map +1 -0
  24. package/config/standard-schema.js +5 -0
  25. package/config/standard-schema.ts +75 -0
  26. package/config/struct-context.d.ts +12 -0
  27. package/config/struct-context.d.ts.map +1 -0
  28. package/config/struct-context.js +4 -0
  29. package/config/struct-context.test.js +33 -0
  30. package/config/struct-context.ts +11 -0
  31. package/config/struct-error.d.ts +26 -0
  32. package/config/struct-error.d.ts.map +1 -0
  33. package/config/struct-error.js +70 -0
  34. package/config/struct-error.test.js +123 -0
  35. package/config/struct-error.ts +95 -0
  36. package/config/structure.d.ts +39 -0
  37. package/config/structure.d.ts.map +1 -0
  38. package/config/structure.js +275 -0
  39. package/{core/structures.test.js → config/structure.test.js} +167 -156
  40. package/config/structure.ts +345 -0
  41. package/configuration.d.ts +2 -0
  42. package/configuration.d.ts.map +1 -0
  43. package/configuration.js +1 -0
  44. package/configuration.ts +1 -0
  45. package/core/README.md +10 -0
  46. package/core/authentication.d.ts.map +1 -1
  47. package/core/authentication.js +1 -1
  48. package/core/authentication.ts +15 -6
  49. package/core/configuration.d.ts +1 -62
  50. package/core/configuration.d.ts.map +1 -1
  51. package/core/configuration.js +1 -239
  52. package/core/configuration.ts +1 -313
  53. package/core/container.d.ts +21 -0
  54. package/core/container.d.ts.map +1 -0
  55. package/core/container.js +50 -0
  56. package/core/container.test.js +95 -0
  57. package/core/container.ts +68 -0
  58. package/core/mod.d.ts +3 -6
  59. package/core/mod.d.ts.map +1 -1
  60. package/core/mod.js +3 -6
  61. package/core/mod.ts +3 -6
  62. package/core/postgres.d.ts +1 -1
  63. package/core/postgres.d.ts.map +1 -1
  64. package/core/postgres.js +1 -1
  65. package/core/postgres.ts +1 -1
  66. package/core/terminator.d.ts.map +1 -1
  67. package/core/terminator.js +11 -0
  68. package/core/terminator.ts +13 -0
  69. package/core/timers.d.ts +1 -0
  70. package/core/timers.d.ts.map +1 -1
  71. package/core/timers.js +2 -1
  72. package/core/timers.ts +4 -0
  73. package/core/tokens.d.ts +11 -0
  74. package/core/tokens.d.ts.map +1 -1
  75. package/core/tokens.js +23 -0
  76. package/core/tokens.test.js +43 -0
  77. package/core/tokens.ts +24 -0
  78. package/core/types.d.ts +3 -2
  79. package/core/types.d.ts.map +1 -1
  80. package/core/types.ts +4 -4
  81. package/core/utilities.d.ts +11 -0
  82. package/core/utilities.d.ts.map +1 -1
  83. package/core/utilities.js +22 -0
  84. package/core/utilities.ts +26 -0
  85. package/core.d.ts +2 -0
  86. package/core.d.ts.map +1 -0
  87. package/core.js +1 -0
  88. package/core.ts +1 -0
  89. package/express-router.d.ts +2 -0
  90. package/express-router.d.ts.map +1 -0
  91. package/express-router.js +1 -0
  92. package/express-router.ts +1 -0
  93. package/http/README.md +10 -0
  94. package/{core → http}/authorization.d.ts +15 -3
  95. package/http/authorization.d.ts.map +1 -0
  96. package/{core → http}/authorization.js +23 -10
  97. package/{core → http}/authorization.test.js +42 -1
  98. package/{core → http}/authorization.ts +54 -17
  99. package/http/cors.d.ts +15 -0
  100. package/http/cors.d.ts.map +1 -0
  101. package/http/cors.js +50 -0
  102. package/http/cors.test.js +134 -0
  103. package/http/cors.ts +79 -0
  104. package/http/define-route.d.ts +28 -0
  105. package/http/define-route.d.ts.map +1 -0
  106. package/http/define-route.js +9 -0
  107. package/http/define-route.test.js +44 -0
  108. package/http/define-route.ts +73 -0
  109. package/http/fetch-router.d.ts +33 -0
  110. package/http/fetch-router.d.ts.map +1 -0
  111. package/{core → http}/fetch-router.js +14 -13
  112. package/http/fetch-router.test.js +297 -0
  113. package/{core → http}/fetch-router.ts +36 -30
  114. package/http/http-error.d.ts +29 -0
  115. package/http/http-error.d.ts.map +1 -0
  116. package/{core/http.js → http/http-error.js} +0 -31
  117. package/{core/http.test.js → http/http-error.test.js} +2 -23
  118. package/http/http-error.ts +65 -0
  119. package/http/mod.d.ts +8 -0
  120. package/http/mod.d.ts.map +1 -0
  121. package/http/mod.js +7 -0
  122. package/http/mod.ts +7 -0
  123. package/http/request-body.d.ts +7 -0
  124. package/http/request-body.d.ts.map +1 -0
  125. package/http/request-body.js +36 -0
  126. package/http/request-body.test.js +73 -0
  127. package/http/request-body.ts +52 -0
  128. package/http.d.ts +2 -0
  129. package/http.d.ts.map +1 -0
  130. package/http.js +1 -0
  131. package/http.ts +1 -0
  132. package/koa-router.d.ts +2 -0
  133. package/koa-router.d.ts.map +1 -0
  134. package/koa-router.js +1 -0
  135. package/koa-router.ts +1 -0
  136. package/mod.d.ts +2 -0
  137. package/mod.d.ts.map +1 -0
  138. package/mod.js +1 -0
  139. package/mod.ts +1 -0
  140. package/node-router.d.ts +2 -0
  141. package/node-router.d.ts.map +1 -0
  142. package/node-router.js +1 -0
  143. package/node-router.ts +1 -0
  144. package/package.json +4 -7
  145. package/polyfill.d.ts +2 -0
  146. package/polyfill.d.ts.map +1 -0
  147. package/polyfill.js +1 -0
  148. package/polyfill.ts +1 -0
  149. package/postgres.d.ts +2 -0
  150. package/postgres.d.ts.map +1 -0
  151. package/postgres.js +1 -0
  152. package/postgres.ts +1 -0
  153. package/source/README.md +10 -0
  154. package/source/configuration.d.ts +4 -3
  155. package/source/configuration.d.ts.map +1 -1
  156. package/source/configuration.js +4 -3
  157. package/source/configuration.ts +5 -3
  158. package/source/core.d.ts.map +1 -1
  159. package/source/core.js +0 -1
  160. package/source/core.ts +0 -1
  161. package/source/express-router.d.ts +1 -1
  162. package/source/express-router.js +1 -1
  163. package/source/express-router.ts +1 -1
  164. package/source/http.d.ts +2 -0
  165. package/source/http.d.ts.map +1 -0
  166. package/source/http.js +1 -0
  167. package/source/http.ts +1 -0
  168. package/source/koa-router.d.ts +1 -1
  169. package/source/koa-router.js +1 -1
  170. package/source/koa-router.ts +1 -1
  171. package/source/mod.d.ts +1 -0
  172. package/source/mod.d.ts.map +1 -1
  173. package/source/mod.js +1 -0
  174. package/source/mod.ts +1 -0
  175. package/source/node-router.d.ts +15 -7
  176. package/source/node-router.d.ts.map +1 -1
  177. package/source/node-router.js +75 -11
  178. package/source/node-router.ts +115 -18
  179. package/source/package-lock.json +4 -4
  180. package/source/package.json +1 -1
  181. package/source/postgres.d.ts +2 -2
  182. package/source/postgres.d.ts.map +1 -1
  183. package/source/postgres.js +2 -2
  184. package/source/postgres.ts +2 -2
  185. package/source/testing.d.ts +2 -0
  186. package/source/testing.d.ts.map +1 -0
  187. package/source/testing.js +1 -0
  188. package/source/testing.ts +1 -0
  189. package/terminator.d.ts +2 -0
  190. package/terminator.d.ts.map +1 -0
  191. package/terminator.js +1 -0
  192. package/terminator.ts +1 -0
  193. package/testing/README.md +10 -0
  194. package/testing/mod.d.ts +5 -0
  195. package/testing/mod.d.ts.map +1 -0
  196. package/testing/mod.js +4 -0
  197. package/testing/mod.ts +4 -0
  198. package/testing/spy.d.ts +5 -0
  199. package/testing/spy.d.ts.map +1 -0
  200. package/testing/spy.js +8 -0
  201. package/testing/spy.ts +14 -0
  202. package/testing/stubs.d.ts +6 -0
  203. package/testing/stubs.d.ts.map +1 -0
  204. package/testing/stubs.js +17 -0
  205. package/testing/stubs.ts +24 -0
  206. package/testing/testing-router.d.ts +9 -0
  207. package/testing/testing-router.d.ts.map +1 -0
  208. package/testing/testing-router.js +25 -0
  209. package/testing/testing-router.ts +34 -0
  210. package/testing/utilities.d.ts +15 -0
  211. package/testing/utilities.d.ts.map +1 -0
  212. package/testing/utilities.js +37 -0
  213. package/testing/utilities.ts +48 -0
  214. package/testing.d.ts +2 -0
  215. package/testing.d.ts.map +1 -0
  216. package/testing.js +1 -0
  217. package/testing.ts +1 -0
  218. package/tsconfig.json +4 -3
  219. package/core/authorization.d.ts.map +0 -1
  220. package/core/fetch-router.d.ts +0 -36
  221. package/core/fetch-router.d.ts.map +0 -1
  222. package/core/fetch-router.test.js +0 -156
  223. package/core/http.d.ts +0 -57
  224. package/core/http.d.ts.map +0 -1
  225. package/core/http.ts +0 -149
  226. package/core/structures.d.ts +0 -41
  227. package/core/structures.d.ts.map +0 -1
  228. package/core/structures.js +0 -251
  229. package/core/structures.ts +0 -303
  230. /package/{core → http}/server-sent-events.d.ts +0 -0
  231. /package/{core → http}/server-sent-events.d.ts.map +0 -0
  232. /package/{core → http}/server-sent-events.js +0 -0
  233. /package/{core → http}/server-sent-events.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -1,7 +1,65 @@
1
+ ---
2
+ permalink: /changelog/
3
+ layout: simple.njk
4
+ ---
5
+
1
6
  # Change log
2
7
 
3
8
  This file documents notable changes to the project
4
9
 
10
+ ## 0.9
11
+
12
+ **features**
13
+
14
+ - Add `config.external`
15
+ - Set "dependencies" on HTTP routes that are evaluated on-demand per request
16
+ - Revamp website
17
+ - Start splitting code out into modules
18
+ - [StandardSchema](https://standardschema.dev) support
19
+ - Add `AuthorizationService#from`
20
+ - Add `Structure.null`
21
+ - Add `Structure.any`
22
+ - Add `Structure.partial`
23
+ - Add `Structure.date`
24
+ - `assertRequestBody` can return a Promise if you pass a request,
25
+ where it will use `getRequestBody` to get the body then validate it
26
+ - Add `createStoppable` to Node.js module and apply it to `serveHTTP`
27
+ - Add `testing` module with utilities, stubs, fakes and a testing router
28
+
29
+ **improved**
30
+
31
+ - More test coverage
32
+ - Better deprecation comments with `@link`
33
+ - Add `SqlDependency#end` method
34
+ - Export `SqlDependency`
35
+ - Simplified `Structure` and `config` generics with nested structures
36
+ - Update `urlpattern-polyfill` to 10.1.0
37
+ - Move to erasable TypeScript code
38
+
39
+ **fixes**
40
+
41
+ - `AbstractAuthorizationService` includes assert's `options` parameter
42
+ - Configuration correctly follows `flags > env > json > fallback`
43
+ - `Structure.object` sets required fields in schema
44
+
45
+ **deprecations**
46
+
47
+ - `StructError` -> `Structure.Error`
48
+
49
+ ## 0.8.0
50
+
51
+ **features**
52
+
53
+ - Add optional `options` parameter to `authz.assert` to check the scope
54
+ - Add experimental `authz.from` method to parse the authenticated user
55
+ - Add experimental `CompositeTokens` to combine multiple TokenServices together
56
+ - Add experimental `Cors` utility for addings CORS headers to responses
57
+ - Add experimental `cors` option to `FetchRouter`
58
+
59
+ **fixes**
60
+
61
+ - Add optional `res` parameter to `getResponseReadable` to propagate stream cancellations
62
+
5
63
  ## 0.7.0
6
64
 
7
65
  **new**
package/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ ---
2
+ permalink: /index.html
3
+ layout: home.njk
4
+ ---
5
+
1
6
  # Gruber
2
7
 
3
8
  An isomorphic JavaScript library for creating web apps.
@@ -333,6 +338,13 @@ const struct = config.object({
333
338
  fallback: "postgres://user:secret@localhost:5432/database",
334
339
  }),
335
340
  }),
341
+
342
+ auth: config.external(
343
+ new URL("./auth.json", import.meta.url),
344
+ config.object({
345
+ users: Structure.array(Structure.string()),
346
+ }),
347
+ ),
336
348
  });
337
349
 
338
350
  // Load the configuration and parse it
@@ -867,6 +879,11 @@ Use it to get a `Response` from the provided request, based on the router's rout
867
879
  const response = await router.getResponse(new Request("http://localhost"));
868
880
  ```
869
881
 
882
+ **experimental**
883
+
884
+ - `options.log` turn on HTTP logging with `true` or a custom
885
+ - `options.cors` apply CORS headers with a [Cors](#cors) instance
886
+
870
887
  #### unstable http
871
888
 
872
889
  There are some unstable internal methods too:
@@ -877,6 +894,31 @@ There are some unstable internal methods too:
877
894
  - `getRequestBody(request)` Get the JSON of FormData body of a request
878
895
  - `assertRequestBody(struct, body)` Assert the body matches a structure and return the parsed value
879
896
 
897
+ #### Cors
898
+
899
+ There is an unstable API for applying [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) headers to responses.
900
+
901
+ ```ts
902
+ import { Cors } from "gruber";
903
+
904
+ const cors = new Cors({
905
+ credentials: true,
906
+ origins: ["http://localhost:8080"],
907
+ });
908
+
909
+ const response = cors.apply(
910
+ new Request("http://localhsot:8000"),
911
+ new Response("ok"),
912
+ );
913
+ ```
914
+
915
+ It returns a clone of the response passed to it with CORS headers applied. You should no longer use the response passed into it. The headers it applies are:
916
+
917
+ - `Access-Control-Allow-Methods` set to `GET, HEAD, PUT, PATCH, POST, DELETE`
918
+ - `Access-Control-Allow-Headers` mirrors what is set on `Access-Control-Request-Headers` and adds that to `Vary`
919
+ - `Access-Control-Allow-Origin` allows the `Origin` if it matches the `origins` parameter
920
+ - `Access-Control-Allow-Credentials` is set to `true` if the `credentials` parameter is
921
+
880
922
  ### Postgres
881
923
 
882
924
  #### getPostgresMigratorOptions
@@ -1063,16 +1105,16 @@ const redis: RedisClientType;
1063
1105
  const store = new RedisStore(redis, { prefix: "/v2" });
1064
1106
  ```
1065
1107
 
1066
- ### JWT
1108
+ ### Tokens
1067
1109
 
1068
- An abstraction around signing a JWT for a user with an access scope.
1110
+ An abstraction around signing or storing a token for a user with an access scope.
1069
1111
  There is currently one implementation using [jose](https://github.com/panva/jose).
1070
1112
 
1071
1113
  ```ts
1072
- import { JoseJwtService } from "gruber";
1114
+ import { JoseTokens } from "gruber";
1073
1115
  import * as jose from "jose";
1074
1116
 
1075
- const jwt = new JoseJwtService(
1117
+ const jwt = new JoseTokens(
1076
1118
  {
1077
1119
  secret: "top_secret",
1078
1120
  issuer: "myapp.io",
@@ -1091,6 +1133,22 @@ const token = await jwt.sign("user:books:read", {
1091
1133
  const parsed = await jwt.verify(token);
1092
1134
  ```
1093
1135
 
1136
+ There is also `CompositeTokens` which lets you combine multiple verifiers with one signer.
1137
+ For example, if your app has several methods a client might authenticate and one way it itself signs things,
1138
+ like a user token, or a static service or database app-token.
1139
+
1140
+ > UNSTABLE
1141
+
1142
+ ```ts
1143
+ import { CompositeTokens, JoseTokens } from "gruber";
1144
+ import * as jose from "jose";
1145
+
1146
+ const tokens = new CompositeTokens(new JoseTokens("..."), [
1147
+ new JoseTokens("..."),
1148
+ // Different token formats your app accepts
1149
+ ]);
1150
+ ```
1151
+
1094
1152
  ### Authorization
1095
1153
 
1096
1154
  > UNSTABLE
@@ -1110,19 +1168,27 @@ const token = authz.getAuthorization(
1110
1168
  }),
1111
1169
  );
1112
1170
 
1113
- // { userId: number | undefined, scope: string }
1171
+ // { kind: 'user', userId: number , scope: string } | { kind: 'service', scope: string }
1172
+ const result = await authz.from(
1173
+ new Request("https://example.com", {
1174
+ headers: { Authorization: "Bearer some-long-secure-token" },
1175
+ }),
1176
+ );
1177
+
1178
+ // { kind: 'user', userId: number , scope: string } | { kind: 'service', scope: string }
1114
1179
  const { userId, scope } = await authz.assert(
1115
1180
  new Request("https://example.com", {
1116
1181
  headers: { Authorization: "Bearer some-long-secure-token" },
1117
1182
  }),
1183
+ { scope: "repo:coffee-club" }, // optional
1118
1184
  );
1119
1185
 
1120
- // { userId: number, scope: string }
1186
+ // { kind: 'user', userId: number, scope: string }
1121
1187
  const { userId, scope } = await authz.assertUser(
1122
1188
  new Request("https://example.com", {
1123
1189
  headers: { Cookie: "my_session=some-long-secure-token" },
1124
1190
  }),
1125
- { scope: "user:books:read" },
1191
+ { scope: "user:books:read" }, // optional
1126
1192
  );
1127
1193
 
1128
1194
  includesScope("user:books:read", "user:books:read"); // true
@@ -1520,11 +1586,18 @@ const server = http.createServer((req) => {
1520
1586
  `getResponseReadable` creates a [streams:Readable](https://nodejs.org/api/stream.html#class-streamreadable) from the body of a fetch Response.
1521
1587
 
1522
1588
  ```js
1589
+ import http from "node:http";
1523
1590
  import { getResponseReadable } from "gruber/node-router.js";
1524
1591
 
1525
- const readable = getResponseReadable(new Response("some body"));
1592
+ const server = http.createServer((req, res) => {
1593
+ const readable = getResponseReadable(new Response("some body"), res);
1594
+ });
1526
1595
  ```
1527
1596
 
1597
+ Pass in `res` if you want the readable to be cancelled if reading the response is aborted.
1598
+
1599
+ > NOTE: This relies on the **experimental** [Readable.fromWeb](https://nodejs.org/api/stream.html#streamreadablefromwebreadablestream-options)
1600
+
1528
1601
  ## Development
1529
1602
 
1530
1603
  WIP stuff
@@ -1537,6 +1610,7 @@ WIP stuff
1537
1610
  1. `cd bundle/node`
1538
1611
  2. `npm publish`
1539
1612
  4. Copy the deno source to the S3 bucket — `bundle/deno` → `esm.r0b.io/gruber@VERSION/`
1613
+ 5. Push to GitHub and create a [release](https://github.com/robb-j/gruber/releases/new)
1540
1614
 
1541
1615
  ### nice snippets
1542
1616
 
@@ -0,0 +1,10 @@
1
+ ---
2
+ permalink: /config/
3
+ layout: simple.njk
4
+ eleventyNavigation:
5
+ key: Config
6
+ ---
7
+
8
+ # Configuration
9
+
10
+ Detailled configuration documentation...
@@ -0,0 +1,33 @@
1
+ import { Structure } from "./structure.ts";
2
+ import { ConfigurationDescription, PrimativeOptions } from "./specifications.ts";
3
+ import { ConfigurationResult } from "./parsers.ts";
4
+ import { StructContext } from "./struct-context.ts";
5
+ export interface ConfigurationOptions {
6
+ readTextFile(url: URL | string): Promise<string | null>;
7
+ getEnvironmentVariable(key: string): string | undefined;
8
+ getCommandArgument(key: string): string | undefined;
9
+ stringify(value: any): string | Promise<string>;
10
+ parse(value: string): any;
11
+ }
12
+ export declare class Configuration {
13
+ static readonly spec: unique symbol;
14
+ options: ConfigurationOptions;
15
+ constructor(options: ConfigurationOptions);
16
+ _loadValue<T>(path: string | URL, structure: Structure<T>, context: StructContext): Promise<T | null>;
17
+ /** Wrap a primativ Structure with configuration logic */
18
+ _primative<T>(struct: Structure<T>, options: PrimativeOptions<T>, deconfigure: (result: ConfigurationResult) => unknown): Structure<T>;
19
+ object<T extends Record<string, unknown>>(fields: {
20
+ [K in keyof T]: Structure<T[K]>;
21
+ }): Structure<T>;
22
+ array<T extends unknown>(item: Structure<T>): Structure<T[]>;
23
+ external<T extends Record<string, unknown> | Array<unknown>>(path: string | URL, struct: Structure<T>): Structure<T>;
24
+ string(options: PrimativeOptions<string>): Structure<string>;
25
+ number(options: PrimativeOptions<number>): Structure<number>;
26
+ boolean(options: PrimativeOptions<boolean>): Structure<boolean>;
27
+ url(options: PrimativeOptions<string | URL>): Structure<URL>;
28
+ load<T>(url: URL | string, struct: Structure<T>): Promise<T>;
29
+ getUsage(struct: unknown, currentValue?: unknown): string;
30
+ describe(value: unknown, prefix?: string): ConfigurationDescription;
31
+ getJSONSchema(struct: Structure<any>): import("./structure.ts").Schema;
32
+ }
33
+ //# sourceMappingURL=configuration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configuration.d.ts","sourceRoot":"","sources":["configuration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAS,MAAM,gBAAgB,CAAC;AAElD,OAAO,EAEN,wBAAwB,EAGxB,gBAAgB,EAEhB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKN,mBAAmB,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,WAAW,oBAAoB;IACpC,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACxD,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACpD,SAAS,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC;CAC1B;AAED,qBAAa,aAAa;IACzB,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgC;IAEpD,OAAO,EAAE,oBAAoB,CAAC;gBAElB,OAAO,EAAE,oBAAoB;IAOnC,UAAU,CAAC,CAAC,EACjB,IAAI,EAAE,MAAM,GAAG,GAAG,EAClB,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,EACvB,OAAO,EAAE,aAAa,GACpB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAgBpB,yDAAyD;IACzD,UAAU,CAAC,CAAC,EACX,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,EACpB,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC5B,WAAW,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,OAAO;IActD,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE;SAChD,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAC/B,GAAG,SAAS,CAAC,CAAC,CAAC;IAgBhB,KAAK,CAAC,CAAC,SAAS,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC;IAW5D,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,EAC1D,IAAI,EAAE,MAAM,GAAG,GAAG,EAClB,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,GAClB,SAAS,CAAC,CAAC,CAAC;IAgCf,MAAM,CAAC,OAAO,EAAE,gBAAgB,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC;IAkB5D,MAAM,CAAC,OAAO,EAAE,gBAAgB,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC;IAgB5D,OAAO,CAAC,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC;IAgB/D,GAAG,CAAC,OAAO,EAAE,gBAAgB,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,SAAS,CAAC,GAAG,CAAC;IAyBtD,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAyBlE,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,OAAO;IAwBhD,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,SAAK,GAAG,wBAAwB;IAM/D,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC;CAGpC"}
@@ -0,0 +1,181 @@
1
+ import { Structure } from "./structure.js";
2
+ import { formatMarkdownTable, PromiseList } from "../core/mod.js";
3
+ import { arraySpec, getSpecification, objectSpec, primativeSpec, } from "./specifications.js";
4
+ import { _parseBoolean, _parseFloat, _parsePrimative, _parseURL, } from "./parsers.js";
5
+ export class Configuration {
6
+ static spec = Symbol("configuration.spec");
7
+ options;
8
+ constructor(options) {
9
+ this.options = options;
10
+ }
11
+ //
12
+ // Utilities
13
+ //
14
+ async _loadValue(path, structure, context) {
15
+ const text = await this.options.readTextFile(path);
16
+ // Catch missing files and return null instead, the consumer can decide what to do
17
+ if (!text)
18
+ return null;
19
+ // Parse the text file
20
+ const value = this.options.parse(text);
21
+ // Remove the JSON schema field if it is set
22
+ delete value.$schema;
23
+ // Process the contents
24
+ return structure.process(value, context);
25
+ }
26
+ /** Wrap a primativ Structure with configuration logic */
27
+ _primative(struct, options, deconfigure) {
28
+ return new Structure(struct.schema, (value, context) => {
29
+ return struct.process(deconfigure(_parsePrimative(this.options, options, value)), context);
30
+ });
31
+ }
32
+ //
33
+ // Types
34
+ //
35
+ object(fields) {
36
+ if (typeof fields !== "object" || fields === null) {
37
+ throw new TypeError("options must be a non-null object");
38
+ }
39
+ for (const key in fields) {
40
+ if (!(fields[key] instanceof Structure)) {
41
+ throw new TypeError(`options[${key}] is not a Structure`);
42
+ }
43
+ }
44
+ const struct = Structure.object(fields);
45
+ Object.defineProperty(struct, Configuration.spec, {
46
+ value: objectSpec(fields),
47
+ });
48
+ return struct;
49
+ }
50
+ array(item) {
51
+ if (!(item instanceof Structure)) {
52
+ throw new TypeError("options is not a Structure");
53
+ }
54
+ const struct = Structure.array(item);
55
+ Object.defineProperty(struct, Configuration.spec, {
56
+ value: arraySpec(item),
57
+ });
58
+ return struct;
59
+ }
60
+ external(path, struct) {
61
+ return new Structure(struct.schema, (value, context) => {
62
+ if (context.type !== "async") {
63
+ throw new SyntaxError("config.external must be used async");
64
+ }
65
+ // Create a dummy value to return for now
66
+ let internal = struct.schema.type === "array" ? [] : {};
67
+ // Register a promise to load the actual value
68
+ context.promises.push(async () => {
69
+ // Try loading the external value
70
+ let loaded = await this._loadValue(path, struct, context);
71
+ // If not found, try the inline value
72
+ if (loaded === null) {
73
+ loaded = struct.process(value, context);
74
+ }
75
+ // Apply the value depending if its an array or not
76
+ // being carful to modifiy the existing reference
77
+ if (struct.schema.type === "array") {
78
+ internal.push(...loaded);
79
+ }
80
+ else {
81
+ Object.assign(internal, loaded);
82
+ }
83
+ });
84
+ return internal;
85
+ });
86
+ }
87
+ string(options) {
88
+ if (typeof options.fallback !== "string") {
89
+ throw new TypeError("options.fallback must be a string");
90
+ }
91
+ const struct = this._primative(Structure.string(), options, (result) => result.value);
92
+ Object.defineProperty(struct, Configuration.spec, {
93
+ value: primativeSpec("string", options),
94
+ });
95
+ return struct;
96
+ }
97
+ number(options) {
98
+ if (typeof options.fallback !== "number") {
99
+ throw new TypeError("options.fallback must be a number");
100
+ }
101
+ const struct = this._primative(Structure.number(), options, (result) => _parseFloat(result));
102
+ Object.defineProperty(struct, Configuration.spec, {
103
+ value: primativeSpec("number", options),
104
+ });
105
+ return struct;
106
+ }
107
+ boolean(options) {
108
+ if (typeof options?.fallback !== "boolean") {
109
+ throw new TypeError("options.fallback must be a boolean");
110
+ }
111
+ const struct = this._primative(Structure.boolean(), options, (result) => _parseBoolean(result));
112
+ Object.defineProperty(struct, Configuration.spec, {
113
+ value: primativeSpec("boolean", options),
114
+ });
115
+ return struct;
116
+ }
117
+ url(options) {
118
+ if (typeof options.fallback !== "string" &&
119
+ !(options.fallback instanceof URL)) {
120
+ throw new TypeError("options.fallback must be a string or URL");
121
+ }
122
+ const opts2 = { ...options, fallback: new URL(options.fallback) };
123
+ const struct = this._primative(Structure.url(), opts2, (result) => _parseURL(result));
124
+ Object.defineProperty(struct, Configuration.spec, {
125
+ value: primativeSpec("url", opts2),
126
+ });
127
+ return struct;
128
+ }
129
+ //
130
+ // Methods
131
+ //
132
+ async load(url, struct) {
133
+ const context = {
134
+ type: "async",
135
+ path: [],
136
+ promises: new PromiseList(),
137
+ };
138
+ // Fail outside the try-catch to surface structure errors
139
+ try {
140
+ let obj = await this._loadValue(url, struct, context);
141
+ if (!obj)
142
+ obj = struct.process({}, context);
143
+ await context.promises.all();
144
+ return obj;
145
+ }
146
+ catch (error) {
147
+ if (error instanceof Structure.Error) {
148
+ console.error(error.toFriendlyString());
149
+ }
150
+ else {
151
+ console.error("Configuration failed to parse");
152
+ }
153
+ throw error;
154
+ }
155
+ }
156
+ getUsage(struct, currentValue) {
157
+ const { fallback, fields } = this.describe(struct);
158
+ const lines = [
159
+ "Usage:",
160
+ "",
161
+ formatMarkdownTable(fields.sort((a, b) => a.name.localeCompare(b.name)), ["name", "type", "flag", "variable", "fallback"], "~"),
162
+ "",
163
+ "",
164
+ "Default:",
165
+ this.options.stringify(fallback),
166
+ ];
167
+ if (currentValue) {
168
+ lines.push("", "", "Current:", JSON.stringify(currentValue, null, 2));
169
+ }
170
+ return lines.join("\n");
171
+ }
172
+ describe(value, prefix = "") {
173
+ const spec = getSpecification(value);
174
+ if (!spec)
175
+ return { fallback: undefined, fields: [] };
176
+ return spec(prefix);
177
+ }
178
+ getJSONSchema(struct) {
179
+ return struct.getSchema();
180
+ }
181
+ }