@getsoda/core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/app/app.d.ts +17 -1
- package/dist/app/app.d.ts.map +1 -1
- package/dist/app/app.js +29 -10
- package/dist/app/app.js.map +1 -1
- package/dist/di/container.d.ts +5 -0
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +15 -1
- package/dist/di/container.js.map +1 -1
- package/dist/di/graph.d.ts +2 -2
- package/dist/di/graph.d.ts.map +1 -1
- package/dist/di/graph.js +20 -10
- package/dist/di/graph.js.map +1 -1
- package/dist/http/arguments.d.ts +20 -2
- package/dist/http/arguments.d.ts.map +1 -1
- package/dist/http/arguments.js +29 -0
- package/dist/http/arguments.js.map +1 -1
- package/dist/http/body.d.ts +28 -0
- package/dist/http/body.d.ts.map +1 -1
- package/dist/http/body.js +96 -1
- package/dist/http/body.js.map +1 -1
- package/dist/http/context-key.d.ts +33 -0
- package/dist/http/context-key.d.ts.map +1 -0
- package/dist/http/context-key.js +37 -0
- package/dist/http/context-key.js.map +1 -0
- package/dist/http/context.d.ts +37 -3
- package/dist/http/context.d.ts.map +1 -1
- package/dist/http/context.js +63 -5
- package/dist/http/context.js.map +1 -1
- package/dist/http/cookies.d.ts +49 -0
- package/dist/http/cookies.d.ts.map +1 -0
- package/dist/http/cookies.js +77 -0
- package/dist/http/cookies.js.map +1 -0
- package/dist/http/cors.d.ts +41 -0
- package/dist/http/cors.d.ts.map +1 -0
- package/dist/http/cors.js +97 -0
- package/dist/http/cors.js.map +1 -0
- package/dist/http/index.d.ts +7 -1
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +4 -1
- package/dist/http/index.js.map +1 -1
- package/dist/http/middleware.d.ts +56 -0
- package/dist/http/middleware.d.ts.map +1 -0
- package/dist/http/middleware.js +83 -0
- package/dist/http/middleware.js.map +1 -0
- package/dist/http/parsers.d.ts +3 -0
- package/dist/http/parsers.d.ts.map +1 -1
- package/dist/http/parsers.js +2 -1
- package/dist/http/parsers.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jwt/algorithms.d.ts +19 -0
- package/dist/jwt/algorithms.d.ts.map +1 -0
- package/dist/jwt/algorithms.js +60 -0
- package/dist/jwt/algorithms.js.map +1 -0
- package/dist/jwt/errors.d.ts +24 -0
- package/dist/jwt/errors.d.ts.map +1 -0
- package/dist/jwt/errors.js +14 -0
- package/dist/jwt/errors.js.map +1 -0
- package/dist/jwt/guard.d.ts +30 -0
- package/dist/jwt/guard.d.ts.map +1 -0
- package/dist/jwt/guard.js +61 -0
- package/dist/jwt/guard.js.map +1 -0
- package/dist/jwt/index.d.ts +14 -0
- package/dist/jwt/index.d.ts.map +1 -0
- package/dist/jwt/index.js +10 -0
- package/dist/jwt/index.js.map +1 -0
- package/dist/jwt/module.d.ts +36 -0
- package/dist/jwt/module.d.ts.map +1 -0
- package/dist/jwt/module.js +38 -0
- package/dist/jwt/module.js.map +1 -0
- package/dist/jwt/service.d.ts +66 -0
- package/dist/jwt/service.d.ts.map +1 -0
- package/dist/jwt/service.js +138 -0
- package/dist/jwt/service.js.map +1 -0
- package/dist/logger/console-logger.d.ts +28 -0
- package/dist/logger/console-logger.d.ts.map +1 -0
- package/dist/logger/console-logger.js +87 -0
- package/dist/logger/console-logger.js.map +1 -0
- package/dist/logger/index.d.ts +9 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +7 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/logger.d.ts +55 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +66 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/routing/types.d.ts +1 -1
- package/dist/routing/types.d.ts.map +1 -1
- package/package.json +6 -12
- package/src/app/app.ts +53 -12
- package/src/di/container.ts +24 -2
- package/src/di/graph.ts +26 -11
- package/src/http/arguments.ts +42 -2
- package/src/http/body.ts +142 -1
- package/src/http/context-key.ts +35 -0
- package/src/http/context.ts +94 -8
- package/src/http/cookies.ts +96 -0
- package/src/http/cors.ts +143 -0
- package/src/http/index.ts +7 -1
- package/src/http/middleware.ts +124 -0
- package/src/http/parsers.ts +2 -1
- package/src/index.ts +1 -0
- package/src/jwt/algorithms.ts +113 -0
- package/src/jwt/errors.ts +30 -0
- package/src/jwt/guard.ts +74 -0
- package/src/jwt/index.ts +14 -0
- package/src/jwt/module.ts +58 -0
- package/src/jwt/service.ts +216 -0
- package/src/logger/console-logger.ts +119 -0
- package/src/logger/index.ts +9 -0
- package/src/logger/logger.ts +96 -0
- package/src/routing/types.ts +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* The logger contract.
|
|
4
|
+
*
|
|
5
|
+
* `Logger` is both the type and the DI token: the application provides one
|
|
6
|
+
* (a `ConsoleLogger` unless told otherwise) and any class injects it.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* class UsersService {
|
|
10
|
+
* readonly #logger = inject(Logger).child('UsersService')
|
|
11
|
+
*
|
|
12
|
+
* create(user: User) {
|
|
13
|
+
* this.#logger.log('User created', { id: user.id })
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Levels are Nest's. Unlike Nest's `Logger`, it is not a static global: each
|
|
19
|
+
* application has its own, and tests replace it with `overrides`.
|
|
20
|
+
*/
|
|
21
|
+
/** Levels, from the most to the least verbose. */
|
|
22
|
+
export const LOG_LEVELS = ['verbose', 'debug', 'log', 'warn', 'error', 'fatal'];
|
|
23
|
+
/**
|
|
24
|
+
* A logger. Implementations only write entries; levels and contexts are handled here.
|
|
25
|
+
*/
|
|
26
|
+
export class Logger {
|
|
27
|
+
verbose(message, data) {
|
|
28
|
+
this.write({ level: 'verbose', message, data, context: undefined });
|
|
29
|
+
}
|
|
30
|
+
debug(message, data) {
|
|
31
|
+
this.write({ level: 'debug', message, data, context: undefined });
|
|
32
|
+
}
|
|
33
|
+
log(message, data) {
|
|
34
|
+
this.write({ level: 'log', message, data, context: undefined });
|
|
35
|
+
}
|
|
36
|
+
warn(message, data) {
|
|
37
|
+
this.write({ level: 'warn', message, data, context: undefined });
|
|
38
|
+
}
|
|
39
|
+
error(message, data) {
|
|
40
|
+
this.write({ level: 'error', message, data, context: undefined });
|
|
41
|
+
}
|
|
42
|
+
fatal(message, data) {
|
|
43
|
+
this.write({ level: 'fatal', message, data, context: undefined });
|
|
44
|
+
}
|
|
45
|
+
/** A logger writing through this one, with `context` on every entry. */
|
|
46
|
+
child(context) {
|
|
47
|
+
return new ContextLogger(this, context);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class ContextLogger extends Logger {
|
|
51
|
+
#parent;
|
|
52
|
+
#context;
|
|
53
|
+
constructor(parent, context) {
|
|
54
|
+
super();
|
|
55
|
+
this.#parent = parent;
|
|
56
|
+
this.#context = context;
|
|
57
|
+
}
|
|
58
|
+
write(entry) {
|
|
59
|
+
this.#parent.write({ ...entry, context: this.#context });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** A logger that writes nothing: `createApp(AppModule, { logger: false })`. */
|
|
63
|
+
export class SilentLogger extends Logger {
|
|
64
|
+
write() { }
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/logger/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kDAAkD;AAClD,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAU,CAAA;AAkBxF;;GAEG;AACH,MAAM,OAAgB,MAAM;IAI1B,OAAO,CAAC,OAAe,EAAE,IAAc;QACrC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACrE,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,IAAc;QACnC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACnE,CAAC;IAED,GAAG,CAAC,OAAe,EAAE,IAAc;QACjC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACjE,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,IAAc;QAClC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IAClE,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,IAAc;QACnC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACnE,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,IAAc;QACnC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;IACnE,CAAC;IAED,wEAAwE;IACxE,KAAK,CAAC,OAAe;QACnB,OAAO,IAAI,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACzC,CAAC;CACF;AAED,MAAM,aAAc,SAAQ,MAAM;IACvB,OAAO,CAAQ;IACf,QAAQ,CAAQ;IAEzB,YAAY,MAAc,EAAE,OAAe;QACzC,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,OAAO,GAAG,MAAM,CAAA;QACrB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,KAAe;QACnB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAA;IAC1D,CAAC;CACF;AAED,+EAA+E;AAC/E,MAAM,OAAO,YAAa,SAAQ,MAAM;IACtC,KAAK,KAAU,CAAC;CACjB"}
|
package/dist/routing/types.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type Params = Record<string, string>;
|
|
|
30
30
|
*
|
|
31
31
|
* `params` may be the very object Bun put on `req.params`: treat it as read-only.
|
|
32
32
|
*/
|
|
33
|
-
export type Handler = (req:
|
|
33
|
+
export type Handler = (req: Bun.BunRequest, params: Params) => Response | Promise<Response>;
|
|
34
34
|
/**
|
|
35
35
|
* Route definition.
|
|
36
36
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/routing/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU,QAAQ,CAAA;AAE/B;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,SAAU,CAAA;AAExC;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAE3C;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/routing/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU,QAAQ,CAAA;AAE/B;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,SAAU,CAAA;AAExC;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAE3C;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAE3F;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE,oFAAoF;IACpF,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEjF;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IAC9C,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC/D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getsoda/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Class-based web framework for Bun: modules, dependency injection and decorators like Nest, on Bun's native router",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"default": "./dist/index.js"
|
|
30
30
|
},
|
|
31
|
+
"./jwt": {
|
|
32
|
+
"types": "./dist/jwt/index.d.ts",
|
|
33
|
+
"default": "./dist/jwt/index.js"
|
|
34
|
+
},
|
|
31
35
|
"./package.json": "./package.json"
|
|
32
36
|
},
|
|
33
37
|
"types": "./dist/index.d.ts",
|
|
@@ -42,16 +46,6 @@
|
|
|
42
46
|
},
|
|
43
47
|
"scripts": {
|
|
44
48
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
45
|
-
"
|
|
46
|
-
"lint": "oxlint",
|
|
47
|
-
"lint:fix": "oxlint --fix",
|
|
48
|
-
"test": "bun test",
|
|
49
|
-
"prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build"
|
|
50
|
-
},
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"@stylistic/eslint-plugin": "5.10.0",
|
|
53
|
-
"@types/bun": "latest",
|
|
54
|
-
"oxlint": "1.93.0",
|
|
55
|
-
"typescript": "7.0.2"
|
|
49
|
+
"prepublishOnly": "cd ../.. && bun run lint && bun run typecheck && bun run test && cd packages/core && bun run build"
|
|
56
50
|
}
|
|
57
51
|
}
|
package/src/app/app.ts
CHANGED
|
@@ -19,11 +19,14 @@
|
|
|
19
19
|
* are being destroyed; here they cannot.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { createContainer } from '../di/index.ts'
|
|
22
|
+
import { createContainer, provide } from '../di/index.ts'
|
|
23
23
|
import type { Container, ContainerOptions, ModuleImport } from '../di/index.ts'
|
|
24
24
|
import { destroyAll, initializeAll } from '../di/lifecycle.ts'
|
|
25
25
|
import { buildRoutes } from '../http/index.ts'
|
|
26
|
-
import type { ControllerRoutesOptions } from '../http/index.ts'
|
|
26
|
+
import type { ControllerRoutesOptions, Middleware } from '../http/index.ts'
|
|
27
|
+
import { errorResponse } from '../http/exceptions.ts'
|
|
28
|
+
import { applyMiddleware, resolveMiddleware } from '../http/middleware.ts'
|
|
29
|
+
import { ConsoleLogger, Logger, SilentLogger } from '../logger/index.ts'
|
|
27
30
|
import { compileRoutes } from '../routing/index.ts'
|
|
28
31
|
import type { CompiledRoutes, CompileOptions } from '../routing/index.ts'
|
|
29
32
|
import { release, takeOver } from './hot.ts'
|
|
@@ -31,7 +34,18 @@ import { release, takeOver } from './hot.ts'
|
|
|
31
34
|
/**
|
|
32
35
|
* Options of `createApp`.
|
|
33
36
|
*/
|
|
34
|
-
export interface AppOptions extends ContainerOptions, ControllerRoutesOptions, CompileOptions {
|
|
37
|
+
export interface AppOptions extends ContainerOptions, ControllerRoutesOptions, CompileOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Middleware run around every request, before routing (404 and preflights
|
|
40
|
+
* included), first one outermost: `[cors(), requestId]`.
|
|
41
|
+
*/
|
|
42
|
+
readonly middleware?: readonly Middleware[]
|
|
43
|
+
/**
|
|
44
|
+
* Logger of the application, injectable everywhere as `inject(Logger)`.
|
|
45
|
+
* Defaults to a `ConsoleLogger`; `false` writes nothing.
|
|
46
|
+
*/
|
|
47
|
+
readonly logger?: Logger | false
|
|
48
|
+
}
|
|
35
49
|
|
|
36
50
|
/**
|
|
37
51
|
* Where to listen. `port: 0` picks a free port.
|
|
@@ -39,6 +53,11 @@ export interface AppOptions extends ContainerOptions, ControllerRoutesOptions, C
|
|
|
39
53
|
export interface ListenOptions {
|
|
40
54
|
readonly port?: number
|
|
41
55
|
readonly hostname?: string
|
|
56
|
+
/**
|
|
57
|
+
* Largest request body accepted, in bytes; bigger ones are refused before any
|
|
58
|
+
* handler runs. Bun's default is 128 MiB.
|
|
59
|
+
*/
|
|
60
|
+
readonly maxRequestBodySize?: number
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
/** Signals `enableShutdownSignals()` listens to by default. */
|
|
@@ -80,11 +99,13 @@ class Application implements App {
|
|
|
80
99
|
#closing: Promise<void> | undefined
|
|
81
100
|
readonly #enhancers: readonly unknown[]
|
|
82
101
|
readonly #signals = new Map<NodeJS.Signals, () => void>()
|
|
102
|
+
readonly #logger: Logger
|
|
83
103
|
|
|
84
|
-
constructor(container: Container, routes: CompiledRoutes, enhancers: readonly unknown[]) {
|
|
104
|
+
constructor(container: Container, routes: CompiledRoutes, enhancers: readonly unknown[], logger: Logger) {
|
|
85
105
|
this.container = container
|
|
86
106
|
this.routes = routes
|
|
87
107
|
this.#enhancers = enhancers
|
|
108
|
+
this.#logger = logger
|
|
88
109
|
}
|
|
89
110
|
|
|
90
111
|
get server(): Bun.Server<undefined> | undefined {
|
|
@@ -100,9 +121,9 @@ class Application implements App {
|
|
|
100
121
|
throw new Error(`The application already listens on ${this.#server.url.href}`)
|
|
101
122
|
}
|
|
102
123
|
|
|
103
|
-
const { port = 0, hostname } = typeof options === 'number' ? { port: options } : options
|
|
124
|
+
const { port = 0, hostname, maxRequestBodySize } = typeof options === 'number' ? { port: options } : options
|
|
104
125
|
|
|
105
|
-
const server = Bun.serve({ port, hostname, ...this.routes })
|
|
126
|
+
const server = Bun.serve({ port, hostname, maxRequestBodySize, ...this.routes })
|
|
106
127
|
|
|
107
128
|
this.#server = server
|
|
108
129
|
|
|
@@ -110,6 +131,8 @@ class Application implements App {
|
|
|
110
131
|
await takeOver(server.port, this)
|
|
111
132
|
}
|
|
112
133
|
|
|
134
|
+
this.#logger.log(`Listening on ${server.url.href}`)
|
|
135
|
+
|
|
113
136
|
return server
|
|
114
137
|
}
|
|
115
138
|
|
|
@@ -151,7 +174,7 @@ class Application implements App {
|
|
|
151
174
|
process.kill(process.pid, signal)
|
|
152
175
|
},
|
|
153
176
|
(error: unknown) => {
|
|
154
|
-
|
|
177
|
+
this.#logger.fatal('Shutdown failed', { error })
|
|
155
178
|
process.exit(1)
|
|
156
179
|
}
|
|
157
180
|
)
|
|
@@ -230,16 +253,34 @@ const destroyEverything = async (enhancers: readonly unknown[], container: Conta
|
|
|
230
253
|
* error first, as `createContainer` does.
|
|
231
254
|
*/
|
|
232
255
|
export const createApp = async (root: ModuleImport, options: AppOptions = {}): Promise<App> => {
|
|
233
|
-
const
|
|
234
|
-
|
|
256
|
+
const logger = options.logger === false ? new SilentLogger() : (options.logger ?? new ConsoleLogger())
|
|
257
|
+
const own = logger.child('Soda')
|
|
258
|
+
const onError =
|
|
259
|
+
options.onError ??
|
|
260
|
+
((error: unknown, req: Request) =>
|
|
261
|
+
own.error('Unhandled error while handling a request', { error, method: req.method, url: req.url }))
|
|
262
|
+
const container = await createContainer(root, {
|
|
263
|
+
...options,
|
|
264
|
+
providers: [...(options.providers ?? []), provide(Logger, { useValue: logger })],
|
|
265
|
+
})
|
|
266
|
+
const enhancers: unknown[] = []
|
|
235
267
|
|
|
236
268
|
try {
|
|
237
|
-
|
|
269
|
+
// Middleware first: it wraps everything, so it is initialized first and destroyed last.
|
|
270
|
+
const middleware = resolveMiddleware(options.middleware ?? [], (fn) => container.runInContext(fn), (instance) => {
|
|
271
|
+
enhancers.push(instance)
|
|
272
|
+
})
|
|
273
|
+
const built = buildRoutes(container, { ...options, onError })
|
|
238
274
|
|
|
239
|
-
enhancers
|
|
275
|
+
enhancers.push(...built.enhancers)
|
|
240
276
|
await initializeAll(enhancers)
|
|
241
277
|
|
|
242
|
-
|
|
278
|
+
const compiled = compileRoutes(built.routes, options)
|
|
279
|
+
const routes = applyMiddleware(middleware, compiled.routes, compiled.fetch, (error, req) =>
|
|
280
|
+
errorResponse(error, (unknown) => onError(unknown, req))
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return new Application(container, routes, enhancers, own)
|
|
243
284
|
} catch (error) {
|
|
244
285
|
try {
|
|
245
286
|
await destroyEverything(enhancers, container)
|
package/src/di/container.ts
CHANGED
|
@@ -19,11 +19,13 @@
|
|
|
19
19
|
import { isThenable } from '../utils/promise.ts'
|
|
20
20
|
import { runInContext } from './context.ts'
|
|
21
21
|
import type { InjectionContext } from './context.ts'
|
|
22
|
-
import
|
|
22
|
+
import { Module } from './decorators.ts'
|
|
23
|
+
import type { DynamicModule, ModuleClass, ModuleImport } from './decorators.ts'
|
|
23
24
|
import { DiError } from './errors.ts'
|
|
24
25
|
import { buildGraph } from './graph.ts'
|
|
25
26
|
import type { ModuleGraph, ModuleNode, ProviderNode } from './graph.ts'
|
|
26
27
|
import { destroyAll, initializeAll } from './lifecycle.ts'
|
|
28
|
+
import { isProviderRecord } from './provider.ts'
|
|
27
29
|
import type { Provider, ProviderDefinition } from './provider.ts'
|
|
28
30
|
import { tokenName } from './token.ts'
|
|
29
31
|
import type { Constructor, Token } from './token.ts'
|
|
@@ -43,6 +45,25 @@ export interface ContainerOptions {
|
|
|
43
45
|
* ```
|
|
44
46
|
*/
|
|
45
47
|
readonly overrides?: readonly Provider[]
|
|
48
|
+
/**
|
|
49
|
+
* Providers visible in every module, as if exported by a global module:
|
|
50
|
+
* what the application itself provides, such as its `Logger`.
|
|
51
|
+
*/
|
|
52
|
+
readonly providers?: readonly Provider[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Module holding `ContainerOptions.providers`. */
|
|
56
|
+
@Module({})
|
|
57
|
+
class ContainerProvidersModule {}
|
|
58
|
+
|
|
59
|
+
const providersModule = (providers: readonly Provider[]): DynamicModule => {
|
|
60
|
+
const exports: Token<unknown>[] = []
|
|
61
|
+
|
|
62
|
+
for (const provider of providers) {
|
|
63
|
+
exports.push(isProviderRecord(provider) ? provider.token : provider)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { module: ContainerProvidersModule, providers, exports, global: true }
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
/**
|
|
@@ -524,7 +545,8 @@ const createAll = async (graph: ModuleGraph, resolver: Resolver): Promise<Contro
|
|
|
524
545
|
* hooks fail too, everything is rethrown as one `AggregateError`, startup error first.
|
|
525
546
|
*/
|
|
526
547
|
export const createContainer = async (root: ModuleImport, options: ContainerOptions = {}): Promise<Container> => {
|
|
527
|
-
const
|
|
548
|
+
const providers = options.providers ?? []
|
|
549
|
+
const graph = await buildGraph(root, options.overrides ?? [], providers.length > 0 ? [providersModule(providers)] : [])
|
|
528
550
|
const resolver = new Resolver(graph)
|
|
529
551
|
|
|
530
552
|
try {
|
package/src/di/graph.ts
CHANGED
|
@@ -139,7 +139,17 @@ class GraphBuilder {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
/**
|
|
143
|
+
* `globals` are global modules added next to `root` (the container's own
|
|
144
|
+
* providers); they are created first, before anything that may inject them.
|
|
145
|
+
*/
|
|
146
|
+
async build(root: ModuleImport, globals: readonly ModuleImport[]): Promise<ModuleGraph> {
|
|
147
|
+
const globalNodes: ModuleNode[] = []
|
|
148
|
+
|
|
149
|
+
for (let i = 0, len = globals.length; i < len; i++) {
|
|
150
|
+
globalNodes.push(await this.#module(globals[i], 'createContainer()', i))
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
const rootNode = await this.#module(root, 'createContainer()', 0)
|
|
144
154
|
|
|
145
155
|
// The queue grows while modules are read: breadth-first over imports.
|
|
@@ -149,8 +159,8 @@ class GraphBuilder {
|
|
|
149
159
|
|
|
150
160
|
this.#assertReplacementsUsed()
|
|
151
161
|
|
|
152
|
-
const modules = orderModules(rootNode)
|
|
153
|
-
const
|
|
162
|
+
const modules = orderModules([...globalNodes, rootNode])
|
|
163
|
+
const globalModules: ModuleNode[] = []
|
|
154
164
|
const providers: ProviderNode[] = []
|
|
155
165
|
const controllers: ProviderNode[] = []
|
|
156
166
|
|
|
@@ -160,14 +170,14 @@ class GraphBuilder {
|
|
|
160
170
|
assertExportsProvided(node)
|
|
161
171
|
|
|
162
172
|
if (node.global) {
|
|
163
|
-
|
|
173
|
+
globalModules.push(node)
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
providers.push(...node.providers.values())
|
|
167
177
|
controllers.push(...node.controllers)
|
|
168
178
|
}
|
|
169
179
|
|
|
170
|
-
return { root: rootNode, modules, globals, providers, controllers }
|
|
180
|
+
return { root: rootNode, modules, globals: globalModules, providers, controllers }
|
|
171
181
|
}
|
|
172
182
|
|
|
173
183
|
/** Node for an `imports` entry, created on first sight. */
|
|
@@ -320,8 +330,8 @@ const assertExportsProvided = (node: ModuleNode): void => {
|
|
|
320
330
|
}
|
|
321
331
|
}
|
|
322
332
|
|
|
323
|
-
/** Modules reachable from `
|
|
324
|
-
const orderModules = (
|
|
333
|
+
/** Modules reachable from `roots`, each after the modules it imports. */
|
|
334
|
+
const orderModules = (roots: readonly ModuleNode[]): ModuleNode[] => {
|
|
325
335
|
const ordered: ModuleNode[] = []
|
|
326
336
|
const visited = new Set<ModuleNode>()
|
|
327
337
|
|
|
@@ -339,15 +349,20 @@ const orderModules = (root: ModuleNode): ModuleNode[] => {
|
|
|
339
349
|
ordered.push(node)
|
|
340
350
|
}
|
|
341
351
|
|
|
342
|
-
|
|
352
|
+
for (const root of roots) {
|
|
353
|
+
visit(root)
|
|
354
|
+
}
|
|
343
355
|
|
|
344
356
|
return ordered
|
|
345
357
|
}
|
|
346
358
|
|
|
347
359
|
/**
|
|
348
|
-
* Builds the module graph rooted at `root
|
|
360
|
+
* Builds the module graph rooted at `root`, with `globals` as extra global modules.
|
|
349
361
|
*
|
|
350
362
|
* `overrides` replace the provider of their token in every module providing it (for tests).
|
|
351
363
|
*/
|
|
352
|
-
export const buildGraph = (
|
|
353
|
-
|
|
364
|
+
export const buildGraph = (
|
|
365
|
+
root: ModuleImport,
|
|
366
|
+
overrides: readonly Provider[],
|
|
367
|
+
globals: readonly ModuleImport[] = []
|
|
368
|
+
): Promise<ModuleGraph> => new GraphBuilder(overrides).build(root, globals)
|
package/src/http/arguments.ts
CHANGED
|
@@ -16,18 +16,21 @@
|
|
|
16
16
|
* called by the framework before the method instead of by the method itself.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import type { FormFields, FormLimits } from './body.ts'
|
|
20
|
+
import type { ContextKey } from './context-key.ts'
|
|
19
21
|
import type { Context, ParamNameOf } from './context.ts'
|
|
20
22
|
import type { Parsed, ValueParser } from './parsers.ts'
|
|
23
|
+
import { isStandardSchema } from './schema.ts'
|
|
21
24
|
import type { InferOutput, StandardSchemaV1 } from './schema.ts'
|
|
22
25
|
|
|
23
|
-
export type ArgumentKind = 'param' | 'query' | 'header' | 'body' | 'context'
|
|
26
|
+
export type ArgumentKind = 'param' | 'query' | 'header' | 'cookie' | 'body' | 'context' | 'value'
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* One argument of a handler: where it comes from and how it is read.
|
|
27
30
|
*/
|
|
28
31
|
export interface Argument<T, K extends ArgumentKind = ArgumentKind, N extends string = string> {
|
|
29
32
|
readonly kind: K
|
|
30
|
-
/** Parameter, query or
|
|
33
|
+
/** Parameter, query, header or cookie name; the key description for `value`; empty for `body` and `context`. */
|
|
31
34
|
readonly name: N
|
|
32
35
|
readonly read: (ctx: Context) => T | Promise<T>
|
|
33
36
|
}
|
|
@@ -78,6 +81,17 @@ export function header(name: string, parser?: ValueParser<unknown>): Argument<un
|
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
/** Cookie. Like `@Cookies('session')` with `cookie-parser`; `signed(signer)` checks a signed one. */
|
|
85
|
+
export function cookie(name: string): Argument<string | undefined, 'cookie'>
|
|
86
|
+
export function cookie<V extends ValueParser<unknown>>(name: string, parser: V): Argument<Parsed<V>, 'cookie'>
|
|
87
|
+
export function cookie(name: string, parser?: ValueParser<unknown>): Argument<unknown, 'cookie'> {
|
|
88
|
+
return {
|
|
89
|
+
kind: 'cookie',
|
|
90
|
+
name,
|
|
91
|
+
read: parser === undefined ? (ctx) => ctx.cookie(name) : (ctx) => ctx.cookie(name, parser),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
/** JSON body, validated by `schema`. Like `@Body() dto: CreateUserDto` with a validation pipe. */
|
|
82
96
|
export function body(): Argument<unknown, 'body'>
|
|
83
97
|
export function body<S extends StandardSchemaV1>(schema: S): Argument<InferOutput<S>, 'body'>
|
|
@@ -91,3 +105,29 @@ export function body(schema?: StandardSchemaV1): Argument<unknown, 'body'> {
|
|
|
91
105
|
|
|
92
106
|
/** The context itself, for what no other argument covers. Like `@Req()`. */
|
|
93
107
|
export const context = (): Argument<Context, 'context'> => ({ kind: 'context', name: '', read: (ctx) => ctx })
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Value a guard or an interceptor attached to the request. Like a custom Nest
|
|
111
|
+
* parameter decorator such as `@User()`.
|
|
112
|
+
*
|
|
113
|
+
* ```ts
|
|
114
|
+
* @Get('me', [value(USER)])
|
|
115
|
+
* me(user: User) {}
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export const value = <T>(key: ContextKey<T>): Argument<T, 'value'> => ({
|
|
119
|
+
kind: 'value',
|
|
120
|
+
name: key.description,
|
|
121
|
+
read: (ctx) => ctx.get(key),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/** Form body, validated by `schema` when given, within `limits`. Like `@Body()` on a form, or Nest's file interceptors with multer limits. */
|
|
125
|
+
export function form(limits?: FormLimits): Argument<FormFields, 'body'>
|
|
126
|
+
export function form<S extends StandardSchemaV1>(schema: S, limits?: FormLimits): Argument<InferOutput<S>, 'body'>
|
|
127
|
+
export function form(schemaOrLimits?: StandardSchemaV1 | FormLimits, limits?: FormLimits): Argument<unknown, 'body'> {
|
|
128
|
+
return {
|
|
129
|
+
kind: 'body',
|
|
130
|
+
name: '',
|
|
131
|
+
read: isStandardSchema(schemaOrLimits) ? (ctx) => ctx.form(schemaOrLimits, limits) : (ctx) => ctx.form(schemaOrLimits),
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/http/body.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* (`"constructor": "Ford"`) is legitimate and kept.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { BadRequestException, UnsupportedMediaTypeException } from './exceptions.ts'
|
|
18
|
+
import { BadRequestException, PayloadTooLargeException, UnsupportedMediaTypeException } from './exceptions.ts'
|
|
19
19
|
|
|
20
20
|
const SUSPECT_PROTO =
|
|
21
21
|
/"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/
|
|
@@ -79,3 +79,144 @@ export const readJsonBody = async (req: Request): Promise<unknown> => {
|
|
|
79
79
|
|
|
80
80
|
return parseJson(text)
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
const FORM_TYPE = /^(?:multipart\/form-data|application\/x-www-form-urlencoded)(?:\s*;|$)/i
|
|
84
|
+
const ARRAY_SUFFIX = '[]'
|
|
85
|
+
|
|
86
|
+
/** Parsed form data, as `Request.formData()` gives it. */
|
|
87
|
+
type FormBody = Awaited<ReturnType<Request['formData']>>
|
|
88
|
+
|
|
89
|
+
/** A form value: text, or a file of a multipart body. */
|
|
90
|
+
export type FormValue = string | File
|
|
91
|
+
|
|
92
|
+
/** Fields of a form; repeated fields and fields named `name[]` are arrays. */
|
|
93
|
+
export type FormFields = Record<string, FormValue | FormValue[]>
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Turns form data into an object without prototype.
|
|
97
|
+
*
|
|
98
|
+
* ```ts
|
|
99
|
+
* 'a=1&a=2' => { a: ['1', '2'] } // repeated fields are kept, not overwritten
|
|
100
|
+
* 'tags[]=x' => { tags: ['x'] } // `[]` always gives an array, even with one value
|
|
101
|
+
* 'name=Ann' => { name: 'Ann' }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
const toFields = (data: FormBody): FormFields => {
|
|
105
|
+
const fields: FormFields = Object.create(null)
|
|
106
|
+
|
|
107
|
+
for (const [rawKey, value] of data) {
|
|
108
|
+
const array = rawKey.endsWith(ARRAY_SUFFIX)
|
|
109
|
+
const key = array ? rawKey.slice(0, -ARRAY_SUFFIX.length) : rawKey
|
|
110
|
+
const current = fields[key]
|
|
111
|
+
|
|
112
|
+
if (current === undefined) {
|
|
113
|
+
fields[key] = array ? [value] : value
|
|
114
|
+
} else if (Array.isArray(current)) {
|
|
115
|
+
current.push(value)
|
|
116
|
+
} else {
|
|
117
|
+
fields[key] = [current, value]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return fields
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Limits of a form body, beyond the server's `maxRequestBodySize`; each one
|
|
126
|
+
* exceeded is a `413`. Like the `limits` of multer and `@fastify/multipart`.
|
|
127
|
+
*/
|
|
128
|
+
export interface FormLimits {
|
|
129
|
+
/** Bytes of the whole body, refused as soon as they are exceeded, before the body is read entirely. */
|
|
130
|
+
readonly maxBodySize?: number
|
|
131
|
+
/** Bytes of each file. */
|
|
132
|
+
readonly maxFileSize?: number
|
|
133
|
+
/** Number of files. */
|
|
134
|
+
readonly maxFiles?: number
|
|
135
|
+
/** Number of fields, files included. */
|
|
136
|
+
readonly maxFields?: number
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The body, read up to `max` bytes: `413` as soon as it is larger. */
|
|
140
|
+
const readUpTo = async (req: Request, max: number): Promise<Uint8Array<ArrayBuffer>> => {
|
|
141
|
+
if (Number(req.headers.get('content-length') ?? 0) > max) {
|
|
142
|
+
throw new PayloadTooLargeException(`Request body is larger than ${max} bytes`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (req.body === null) {
|
|
146
|
+
return new Uint8Array()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const chunks: Uint8Array[] = []
|
|
150
|
+
let size = 0
|
|
151
|
+
|
|
152
|
+
for await (const chunk of req.body) {
|
|
153
|
+
size += chunk.byteLength
|
|
154
|
+
|
|
155
|
+
if (size > max) {
|
|
156
|
+
throw new PayloadTooLargeException(`Request body is larger than ${max} bytes`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
chunks.push(chunk)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return Buffer.concat(chunks)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** `413` for the first limit the form exceeds. */
|
|
166
|
+
const checkLimits = (data: FormBody, limits: FormLimits): void => {
|
|
167
|
+
const { maxFileSize = Infinity, maxFiles = Infinity, maxFields = Infinity } = limits
|
|
168
|
+
let fields = 0
|
|
169
|
+
let files = 0
|
|
170
|
+
|
|
171
|
+
for (const [name, value] of data) {
|
|
172
|
+
fields++
|
|
173
|
+
|
|
174
|
+
if (fields > maxFields) {
|
|
175
|
+
throw new PayloadTooLargeException(`Form has more than ${maxFields} fields`)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (typeof value !== 'string') {
|
|
179
|
+
files++
|
|
180
|
+
|
|
181
|
+
if (files > maxFiles) {
|
|
182
|
+
throw new PayloadTooLargeException(`Form has more than ${maxFiles} files`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (value.size > maxFileSize) {
|
|
186
|
+
throw new PayloadTooLargeException(`File "${name}" is larger than ${maxFileSize} bytes`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reads a `multipart/form-data` or `application/x-www-form-urlencoded` body:
|
|
194
|
+
* `415` for another content type, `400` for a malformed body, `413` past a limit.
|
|
195
|
+
*
|
|
196
|
+
* Kept apart from JSON on purpose: an HTML form on another site can post a form
|
|
197
|
+
* without a CORS preflight, a JSON body cannot. A JSON endpoint must not accept forms.
|
|
198
|
+
*
|
|
199
|
+
* `File.type` is the type the client declared, not checked against the content.
|
|
200
|
+
*/
|
|
201
|
+
export const readFormBody = async (req: Request, limits: FormLimits = {}): Promise<FormFields> => {
|
|
202
|
+
const type = req.headers.get('content-type')
|
|
203
|
+
|
|
204
|
+
if (type === null || !FORM_TYPE.test(type)) {
|
|
205
|
+
throw new UnsupportedMediaTypeException(`Expected a form body, got ${type ?? 'no content type'}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Parsed from the bytes read so far, as only a whole body can be parsed.
|
|
209
|
+
const source =
|
|
210
|
+
limits.maxBodySize === undefined ? req : new Response(await readUpTo(req, limits.maxBodySize), { headers: { 'content-type': type } })
|
|
211
|
+
let data: FormBody
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
data = await source.formData()
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new BadRequestException('Request body is not a valid form', { cause: error })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
checkLimits(data, limits)
|
|
220
|
+
|
|
221
|
+
return toFields(data)
|
|
222
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Typed keys for values attached to a request: what a guard found (the user,
|
|
4
|
+
* the tenant), read later by interceptors and the handler.
|
|
5
|
+
*
|
|
6
|
+
* Like Angular's `HttpContextToken`: the key is an object, so two keys can
|
|
7
|
+
* never collide, and it carries the value type. Unlike it, a default is
|
|
8
|
+
* optional: reading a key nobody set and that has no default is a programming
|
|
9
|
+
* error (a guard missing on the route), reported as such.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* export const USER = new ContextKey<User>('user')
|
|
13
|
+
*
|
|
14
|
+
* // guard
|
|
15
|
+
* ctx.set(USER, await this.#auth.verify(ctx.header('authorization')))
|
|
16
|
+
*
|
|
17
|
+
* // handler
|
|
18
|
+
* const user = ctx.get(USER)
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Key of a value attached to a request.
|
|
24
|
+
*/
|
|
25
|
+
export class ContextKey<T> {
|
|
26
|
+
constructor(
|
|
27
|
+
readonly description: string,
|
|
28
|
+
/** Value of the key when nothing was set. Without it, reading an unset key throws. */
|
|
29
|
+
readonly defaultValue?: () => T
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
toString(): string {
|
|
33
|
+
return `ContextKey(${this.description})`
|
|
34
|
+
}
|
|
35
|
+
}
|