@shaferllc/keel 0.12.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/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/core/application.d.ts +40 -0
- package/dist/core/application.js +115 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +55 -0
- package/dist/core/container.d.ts +30 -0
- package/dist/core/container.js +59 -0
- package/dist/core/exceptions.d.ts +26 -0
- package/dist/core/exceptions.js +56 -0
- package/dist/core/helpers.d.ts +42 -0
- package/dist/core/helpers.js +55 -0
- package/dist/core/http/kernel.d.ts +29 -0
- package/dist/core/http/kernel.js +173 -0
- package/dist/core/http/router.d.ts +160 -0
- package/dist/core/http/router.js +344 -0
- package/dist/core/index.d.ts +21 -0
- package/dist/core/index.js +13 -0
- package/dist/core/inertia.d.ts +34 -0
- package/dist/core/inertia.js +68 -0
- package/dist/core/provider.d.ts +16 -0
- package/dist/core/provider.js +16 -0
- package/dist/core/request.d.ts +76 -0
- package/dist/core/request.js +151 -0
- package/dist/core/validation.d.ts +32 -0
- package/dist/core/validation.js +33 -0
- package/dist/core/view.d.ts +29 -0
- package/dist/core/view.js +27 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Shafer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Keel ⚓
|
|
4
|
+
|
|
5
|
+
**The house framework for Node.js.**
|
|
6
|
+
|
|
7
|
+
TypeScript · a real service container · convention-driven structure · a code-generating console.
|
|
8
|
+
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
[](https://nodejs.org)
|
|
11
|
+
[](https://www.typescriptlang.org)
|
|
12
|
+
|
|
13
|
+
[Getting Started](./docs/getting-started.md) ·
|
|
14
|
+
[Container](./docs/container.md) ·
|
|
15
|
+
[Routing](./docs/routing.md) ·
|
|
16
|
+
[Providers](./docs/providers.md) ·
|
|
17
|
+
[Console](./docs/console.md)
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
Keel gives you the ergonomics that make you productive — a service container,
|
|
24
|
+
service providers, dot-notation config, expressive routing, and a
|
|
25
|
+
code-generating console — on a modern TypeScript stack. [Hono](https://hono.dev)
|
|
26
|
+
powers the HTTP layer under the hood; everything above it is Keel's.
|
|
27
|
+
|
|
28
|
+
It is a **house framework**: small enough to read in an afternoon, opinionated
|
|
29
|
+
enough to ship on, and yours to extend.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// routes/web.ts
|
|
33
|
+
router.get("/", [HomeController, "index"]);
|
|
34
|
+
router.get("/hello/:name", (c) => c.text(`Hello, ${c.req.param("name")}!`));
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// A controller — resolved from the container, so it gets dependency injection
|
|
39
|
+
export class HomeController {
|
|
40
|
+
constructor(private app: Container) {}
|
|
41
|
+
|
|
42
|
+
index(c: Ctx) {
|
|
43
|
+
return c.json({ app: config("app.name") });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Why Keel?
|
|
49
|
+
|
|
50
|
+
- **A real service container.** `bind` / `singleton` / `instance` / `make`.
|
|
51
|
+
Everything resolves through it — the pattern that keeps apps testable and
|
|
52
|
+
composable.
|
|
53
|
+
- **Service providers.** A `register()` → `boot()` lifecycle to configure the
|
|
54
|
+
app in one place.
|
|
55
|
+
- **Convention over configuration.** `app/`, `config/`, `routes/`, `bootstrap/`
|
|
56
|
+
— you always know where things live.
|
|
57
|
+
- **A code-generating console.** `keel serve`, `keel routes`, and `make:*`
|
|
58
|
+
generators.
|
|
59
|
+
- **Typed end to end.** Strict TypeScript, no build step in dev (powered by
|
|
60
|
+
`tsx`).
|
|
61
|
+
- **Thin and legible.** The whole framework is a few hundred lines in
|
|
62
|
+
`src/core/`. No magic you can't read.
|
|
63
|
+
|
|
64
|
+
## Two repos: the framework and your app
|
|
65
|
+
|
|
66
|
+
Keel is distributed the same way most frameworks are — a **library** you install,
|
|
67
|
+
plus an **app** that depends on it:
|
|
68
|
+
|
|
69
|
+
| Repo | Role |
|
|
70
|
+
|------|------|
|
|
71
|
+
| [`shaferllc/keel`](https://github.com/shaferllc/keel) (this repo) | The framework. Published as `@shaferllc/keel`. |
|
|
72
|
+
| [`shaferllc/keel-app`](https://github.com/shaferllc/keel-app) | The starter app — clone it to build something. Gets core updates via `npm update`. |
|
|
73
|
+
|
|
74
|
+
## Install in your app
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install @shaferllc/keel
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { Application, Router, config } from "@shaferllc/keel/core";
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or clone the [starter app](https://github.com/shaferllc/keel-app) and start
|
|
85
|
+
from a working skeleton.
|
|
86
|
+
|
|
87
|
+
## Hack on the framework itself
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/shaferllc/keel.git
|
|
91
|
+
cd keel
|
|
92
|
+
npm install
|
|
93
|
+
npm run dev # example app on http://localhost:3000
|
|
94
|
+
npm run build # compile the package to dist/
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
curl localhost:3000/ # HomeController@index (JSON)
|
|
99
|
+
curl localhost:3000/ping # inline closure route
|
|
100
|
+
curl localhost:3000/hello/Tom # route parameter
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## The console
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run keel routes # list every registered route
|
|
107
|
+
npm run keel serve --port 8080 # start the server on a chosen port
|
|
108
|
+
npm run keel make:controller Post # -> app/Controllers/PostController.ts
|
|
109
|
+
npm run keel make:provider Billing # -> app/Providers/BillingServiceProvider.ts
|
|
110
|
+
npm run keel make:middleware Auth # -> app/Http/Middleware/authMiddleware.ts
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Under the hood the console binary is `bin/keel.ts`; the npm scripts wrap it with
|
|
114
|
+
`tsx`.
|
|
115
|
+
|
|
116
|
+
## Project layout
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
src/core/ The framework (destined to become @keel/core on npm)
|
|
120
|
+
├─ container.ts Service container — bind / singleton / instance / make
|
|
121
|
+
├─ application.ts Kernel: env + config loading + provider lifecycle
|
|
122
|
+
├─ config.ts Dot-notation config repository + env() helper
|
|
123
|
+
├─ provider.ts ServiceProvider base class (register / boot)
|
|
124
|
+
├─ http/
|
|
125
|
+
│ ├─ router.ts Route facade (closures or [Controller, method] tuples)
|
|
126
|
+
│ └─ kernel.ts Global middleware + compiles routes onto Hono
|
|
127
|
+
├─ cli/ The `keel` console and make: stubs
|
|
128
|
+
└─ index.ts Public surface — userland imports "@keel/core"
|
|
129
|
+
|
|
130
|
+
app/ Your application code
|
|
131
|
+
├─ Controllers/ Resolved from the container (dependency injection)
|
|
132
|
+
├─ Providers/ Register + wire services
|
|
133
|
+
└─ Http/
|
|
134
|
+
├─ Kernel.ts Register global middleware here
|
|
135
|
+
└─ Middleware/
|
|
136
|
+
|
|
137
|
+
bootstrap/ createApplication(): boots providers, loads routes
|
|
138
|
+
├─ app.ts
|
|
139
|
+
└─ providers.ts The list of providers to load
|
|
140
|
+
|
|
141
|
+
config/ config/app.ts -> config('app.*')
|
|
142
|
+
routes/web.ts Route definitions
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `app/` vs `src/core/` split mirrors the classic application-vs-library
|
|
146
|
+
separation: your code in `app/`, the framework in `src/core/`.
|
|
147
|
+
|
|
148
|
+
## The request lifecycle
|
|
149
|
+
|
|
150
|
+
1. `bin/keel.ts serve` calls `createApplication()` in `bootstrap/app.ts`.
|
|
151
|
+
2. The `Application` loads `.env`, then every `config/*.ts` file, then runs each
|
|
152
|
+
provider's `register()` and `boot()`.
|
|
153
|
+
3. The HTTP kernel (`app/Http/Kernel.ts`) applies global middleware and compiles
|
|
154
|
+
the collected routes onto a Hono instance.
|
|
155
|
+
4. `@hono/node-server` serves it. Each request flows through middleware →
|
|
156
|
+
route handler (a closure or a container-resolved controller) → response.
|
|
157
|
+
|
|
158
|
+
See [docs/architecture.md](./docs/architecture.md) for the full picture.
|
|
159
|
+
|
|
160
|
+
## Documentation
|
|
161
|
+
|
|
162
|
+
| Guide | What it covers |
|
|
163
|
+
|-------|----------------|
|
|
164
|
+
| [Getting Started](./docs/getting-started.md) | Install, run, first route and controller |
|
|
165
|
+
| [The Service Container](./docs/container.md) | Binding and resolving services, DI |
|
|
166
|
+
| [Service Providers](./docs/providers.md) | The register/boot lifecycle |
|
|
167
|
+
| [Configuration](./docs/configuration.md) | `config/*.ts`, dot-notation, `env()` |
|
|
168
|
+
| [Routing](./docs/routing.md) | Closures, controller tuples, parameters |
|
|
169
|
+
| [Views](./docs/views.md) | Hono JSX components, layouts, the View service |
|
|
170
|
+
| [Middleware](./docs/middleware.md) | Global middleware, writing your own |
|
|
171
|
+
| [Errors](./docs/errors.md) | HTTP exceptions, debug page, custom handlers |
|
|
172
|
+
| [Validation](./docs/validation.md) | `validate()` with Zod, auto-422 field errors |
|
|
173
|
+
| [Inertia](./docs/inertia.md) | Server-side Inertia.js adapter |
|
|
174
|
+
| [The Console](./docs/console.md) | `serve`, `routes`, `make:*` generators |
|
|
175
|
+
| [Architecture](./docs/architecture.md) | Container, kernel, request lifecycle |
|
|
176
|
+
|
|
177
|
+
## Testing
|
|
178
|
+
|
|
179
|
+
The core has a test suite (Node's built-in runner + `tsx`, no extra tooling):
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npm test # run the suite
|
|
183
|
+
npm run test:coverage # with v8 coverage
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Unit tests cover the container, config, view, exceptions, and validation; an
|
|
187
|
+
integration suite drives the HTTP kernel end-to-end (routing, request/response
|
|
188
|
+
helpers, error rendering, middleware, validation). Coverage sits at **~99%
|
|
189
|
+
lines / ~91% branches**.
|
|
190
|
+
|
|
191
|
+
## Requirements
|
|
192
|
+
|
|
193
|
+
- Node.js **≥ 22**
|
|
194
|
+
- No database or build step required for the MVP core.
|
|
195
|
+
|
|
196
|
+
## Roadmap
|
|
197
|
+
|
|
198
|
+
Keel's first release is the **MVP core**: container, routing, middleware,
|
|
199
|
+
config, and the console. On deck:
|
|
200
|
+
|
|
201
|
+
- [x] View / templating layer (Hono JSX) — **v0.2.0**
|
|
202
|
+
- [x] Cloudflare Workers–safe core — **v0.2.0**
|
|
203
|
+
- [x] Global helpers: `config()`, `app()`, `view()` — **v0.3.0 / v0.4.0**
|
|
204
|
+
- [x] Error & exception handling — **v0.5.0**
|
|
205
|
+
- [x] Request/response + container helpers — **v0.6.0–v0.9.0**
|
|
206
|
+
- [x] Validation (Zod-compatible) — **v0.10.0**
|
|
207
|
+
- [x] First-class routing (groups, resources, named routes) — **v0.11.0**
|
|
208
|
+
- [x] Domain routing, matchers, Inertia adapter — **v0.12.0**
|
|
209
|
+
- [x] Test suite (~99% coverage)
|
|
210
|
+
- [ ] ORM / query builder + migrations (wrapping Drizzle)
|
|
211
|
+
- [ ] Queues (BullMQ), events, and mail
|
|
212
|
+
- [ ] Publish `src/core` as the `@keel/core` package
|
|
213
|
+
|
|
214
|
+
See [CHANGELOG.md](./CHANGELOG.md) for release history.
|
|
215
|
+
|
|
216
|
+
## Contributing
|
|
217
|
+
|
|
218
|
+
Issues and PRs welcome. Run `npm run typecheck` before opening a PR.
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
[MIT](./LICENSE) © 2026 Tom Shafer
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Application is the container plus a lifecycle: it loads env + config,
|
|
3
|
+
* registers service providers, then boots them. This is Keel's kernel.
|
|
4
|
+
*
|
|
5
|
+
* The core is platform-neutral: Node built-ins (fs/path/url) and dotenv are
|
|
6
|
+
* imported dynamically and only when filesystem discovery is enabled, so the
|
|
7
|
+
* same Application runs under Node and on Cloudflare Workers. On Workers, call
|
|
8
|
+
* `boot(providers, { discoverConfig: false, config })` and pass config inline.
|
|
9
|
+
*/
|
|
10
|
+
import { Container } from "./container.js";
|
|
11
|
+
import { Config, type ConfigData } from "./config.js";
|
|
12
|
+
import { Router } from "./http/router.js";
|
|
13
|
+
import { View } from "./view.js";
|
|
14
|
+
import { type ProviderClass } from "./provider.js";
|
|
15
|
+
export interface BootOptions {
|
|
16
|
+
/** Auto-load .env and config/*.ts from the filesystem. Default: true (Node). */
|
|
17
|
+
discoverConfig?: boolean;
|
|
18
|
+
/** Config to merge in directly — the way to configure on Workers. */
|
|
19
|
+
config?: ConfigData;
|
|
20
|
+
}
|
|
21
|
+
export declare class Application extends Container {
|
|
22
|
+
readonly basePath: string;
|
|
23
|
+
private providers;
|
|
24
|
+
private booted;
|
|
25
|
+
constructor(basePath?: string);
|
|
26
|
+
path(...segments: string[]): string;
|
|
27
|
+
config(): Config;
|
|
28
|
+
router(): Router;
|
|
29
|
+
view(): View;
|
|
30
|
+
/** Merge a config object into the repository under its top-level keys. */
|
|
31
|
+
private mergeConfig;
|
|
32
|
+
/** Load .env via dotenv (Node only; no-op elsewhere). */
|
|
33
|
+
private loadEnv;
|
|
34
|
+
/** Load every /config/*.ts file under its filename key (Node only). */
|
|
35
|
+
private loadConfig;
|
|
36
|
+
/** Register a provider instance (defers boot until boot()). */
|
|
37
|
+
register(Provider: ProviderClass): this;
|
|
38
|
+
/** Load config, register providers, then boot them. */
|
|
39
|
+
boot(providers?: ProviderClass[], options?: BootOptions): Promise<this>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Application is the container plus a lifecycle: it loads env + config,
|
|
3
|
+
* registers service providers, then boots them. This is Keel's kernel.
|
|
4
|
+
*
|
|
5
|
+
* The core is platform-neutral: Node built-ins (fs/path/url) and dotenv are
|
|
6
|
+
* imported dynamically and only when filesystem discovery is enabled, so the
|
|
7
|
+
* same Application runs under Node and on Cloudflare Workers. On Workers, call
|
|
8
|
+
* `boot(providers, { discoverConfig: false, config })` and pass config inline.
|
|
9
|
+
*/
|
|
10
|
+
import { Container } from "./container.js";
|
|
11
|
+
import { Config } from "./config.js";
|
|
12
|
+
import { Router } from "./http/router.js";
|
|
13
|
+
import { View } from "./view.js";
|
|
14
|
+
import { setApplication } from "./helpers.js";
|
|
15
|
+
export class Application extends Container {
|
|
16
|
+
basePath;
|
|
17
|
+
providers = [];
|
|
18
|
+
booted = false;
|
|
19
|
+
constructor(basePath = ".") {
|
|
20
|
+
super();
|
|
21
|
+
this.basePath = basePath;
|
|
22
|
+
// Make this the active application for global helpers (config(), app()).
|
|
23
|
+
setApplication(this);
|
|
24
|
+
// Core framework bindings (all platform-neutral).
|
|
25
|
+
this.instance(Application, this);
|
|
26
|
+
this.singleton(Config, () => new Config());
|
|
27
|
+
this.singleton(Router, (app) => new Router(app));
|
|
28
|
+
this.singleton(View, () => new View());
|
|
29
|
+
}
|
|
30
|
+
path(...segments) {
|
|
31
|
+
return [this.basePath, ...segments].join("/");
|
|
32
|
+
}
|
|
33
|
+
config() {
|
|
34
|
+
return this.make(Config);
|
|
35
|
+
}
|
|
36
|
+
router() {
|
|
37
|
+
return this.make(Router);
|
|
38
|
+
}
|
|
39
|
+
view() {
|
|
40
|
+
return this.make(View);
|
|
41
|
+
}
|
|
42
|
+
/** Merge a config object into the repository under its top-level keys. */
|
|
43
|
+
mergeConfig(data) {
|
|
44
|
+
const repo = this.make(Config);
|
|
45
|
+
for (const [key, value] of Object.entries(data)) {
|
|
46
|
+
repo.set(key, value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Load .env via dotenv (Node only; no-op elsewhere). */
|
|
50
|
+
async loadEnv() {
|
|
51
|
+
try {
|
|
52
|
+
const [{ config }, { join }] = await Promise.all([
|
|
53
|
+
import("dotenv"),
|
|
54
|
+
import("node:path"),
|
|
55
|
+
]);
|
|
56
|
+
config({ path: join(this.basePath, ".env") });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Not on Node / no dotenv — fine.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Load every /config/*.ts file under its filename key (Node only). */
|
|
63
|
+
async loadConfig() {
|
|
64
|
+
try {
|
|
65
|
+
const [{ readdir }, { join }, { pathToFileURL }] = await Promise.all([
|
|
66
|
+
import("node:fs/promises"),
|
|
67
|
+
import("node:path"),
|
|
68
|
+
import("node:url"),
|
|
69
|
+
]);
|
|
70
|
+
const dir = this.path("config");
|
|
71
|
+
const repo = this.make(Config);
|
|
72
|
+
const files = await readdir(dir);
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
if (!/\.(ts|js|mjs)$/.test(file))
|
|
75
|
+
continue;
|
|
76
|
+
const key = file.replace(/\.(ts|js|mjs)$/, "");
|
|
77
|
+
const mod = await import(pathToFileURL(join(dir, file)).href);
|
|
78
|
+
repo.set(key, mod.default ?? mod);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// No config dir or not on Node — fine.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Register a provider instance (defers boot until boot()). */
|
|
86
|
+
register(Provider) {
|
|
87
|
+
const provider = new Provider(this);
|
|
88
|
+
this.providers.push(provider);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
/** Load config, register providers, then boot them. */
|
|
92
|
+
async boot(providers = [], options = {}) {
|
|
93
|
+
if (this.booted)
|
|
94
|
+
return this;
|
|
95
|
+
const { discoverConfig = true, config } = options;
|
|
96
|
+
if (discoverConfig) {
|
|
97
|
+
await this.loadEnv();
|
|
98
|
+
await this.loadConfig();
|
|
99
|
+
}
|
|
100
|
+
if (config) {
|
|
101
|
+
this.mergeConfig(config);
|
|
102
|
+
}
|
|
103
|
+
for (const Provider of providers) {
|
|
104
|
+
this.register(Provider);
|
|
105
|
+
}
|
|
106
|
+
for (const provider of this.providers) {
|
|
107
|
+
await provider.register();
|
|
108
|
+
}
|
|
109
|
+
for (const provider of this.providers) {
|
|
110
|
+
await provider.boot();
|
|
111
|
+
}
|
|
112
|
+
this.booted = true;
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config repository with dot-notation access.
|
|
3
|
+
*
|
|
4
|
+
* Config files live in /config and export a default object. They are loaded
|
|
5
|
+
* at boot and merged under their filename: config/app.ts -> config('app.*').
|
|
6
|
+
*/
|
|
7
|
+
export type ConfigData = Record<string, unknown>;
|
|
8
|
+
export declare class Config {
|
|
9
|
+
private items;
|
|
10
|
+
constructor(items?: ConfigData);
|
|
11
|
+
/** config('app.name') or config('app.name', 'Fallback'). */
|
|
12
|
+
get<T = unknown>(key: string, fallback?: T): T;
|
|
13
|
+
set(key: string, value: unknown): void;
|
|
14
|
+
all(): ConfigData;
|
|
15
|
+
}
|
|
16
|
+
/** Read an env var with an optional typed fallback (true/false/number coerced). */
|
|
17
|
+
export declare function env<T = string>(key: string, fallback?: T): T;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config repository with dot-notation access.
|
|
3
|
+
*
|
|
4
|
+
* Config files live in /config and export a default object. They are loaded
|
|
5
|
+
* at boot and merged under their filename: config/app.ts -> config('app.*').
|
|
6
|
+
*/
|
|
7
|
+
export class Config {
|
|
8
|
+
items;
|
|
9
|
+
constructor(items = {}) {
|
|
10
|
+
this.items = items;
|
|
11
|
+
}
|
|
12
|
+
/** config('app.name') or config('app.name', 'Fallback'). */
|
|
13
|
+
get(key, fallback) {
|
|
14
|
+
const segments = key.split(".");
|
|
15
|
+
let current = this.items;
|
|
16
|
+
for (const segment of segments) {
|
|
17
|
+
if (current !== null && typeof current === "object" && segment in current) {
|
|
18
|
+
current = current[segment];
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
26
|
+
set(key, value) {
|
|
27
|
+
const segments = key.split(".");
|
|
28
|
+
const last = segments.pop();
|
|
29
|
+
let current = this.items;
|
|
30
|
+
for (const segment of segments) {
|
|
31
|
+
if (typeof current[segment] !== "object" || current[segment] === null) {
|
|
32
|
+
current[segment] = {};
|
|
33
|
+
}
|
|
34
|
+
current = current[segment];
|
|
35
|
+
}
|
|
36
|
+
current[last] = value;
|
|
37
|
+
}
|
|
38
|
+
all() {
|
|
39
|
+
return this.items;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Read an env var with an optional typed fallback (true/false/number coerced). */
|
|
43
|
+
export function env(key, fallback) {
|
|
44
|
+
const raw = process.env[key];
|
|
45
|
+
if (raw === undefined)
|
|
46
|
+
return fallback;
|
|
47
|
+
if (raw === "true")
|
|
48
|
+
return true;
|
|
49
|
+
if (raw === "false")
|
|
50
|
+
return false;
|
|
51
|
+
if (raw !== "" && !Number.isNaN(Number(raw)) && typeof fallback === "number") {
|
|
52
|
+
return Number(raw);
|
|
53
|
+
}
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The service container — the backbone of the framework.
|
|
3
|
+
*
|
|
4
|
+
* Everything (config, router, controllers, your own services) is resolved
|
|
5
|
+
* through here — it is the single registry every service resolves out of.
|
|
6
|
+
*
|
|
7
|
+
* Bindings are keyed by a string/symbol token OR a class constructor. A
|
|
8
|
+
* factory receives the container so it can resolve its own dependencies.
|
|
9
|
+
*/
|
|
10
|
+
export type Token<T = unknown> = string | symbol | Constructor<T>;
|
|
11
|
+
export type Constructor<T = unknown> = new (...args: any[]) => T;
|
|
12
|
+
export type Factory<T> = (app: Container) => T;
|
|
13
|
+
export declare class Container {
|
|
14
|
+
private bindings;
|
|
15
|
+
private instances;
|
|
16
|
+
/** Register a transient binding — a fresh value every resolve. */
|
|
17
|
+
bind<T>(token: Token<T>, factory: Factory<T>): this;
|
|
18
|
+
/** Register a shared binding — resolved once, then cached. */
|
|
19
|
+
singleton<T>(token: Token<T>, factory: Factory<T>): this;
|
|
20
|
+
/** Register an already-constructed value as a shared instance. */
|
|
21
|
+
instance<T>(token: Token<T>, value: T): T;
|
|
22
|
+
/** True if the token is bound or has a cached instance. */
|
|
23
|
+
bound(token: Token): boolean;
|
|
24
|
+
/** Resolve a token out of the container. */
|
|
25
|
+
make<T>(token: Token<T>): T;
|
|
26
|
+
/** Instantiate a class, giving its constructor the container. */
|
|
27
|
+
build<T>(ctor: Constructor<T>): T;
|
|
28
|
+
/** Alias `app(token)` sugar over `make`. */
|
|
29
|
+
get<T>(token: Token<T>): T;
|
|
30
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The service container — the backbone of the framework.
|
|
3
|
+
*
|
|
4
|
+
* Everything (config, router, controllers, your own services) is resolved
|
|
5
|
+
* through here — it is the single registry every service resolves out of.
|
|
6
|
+
*
|
|
7
|
+
* Bindings are keyed by a string/symbol token OR a class constructor. A
|
|
8
|
+
* factory receives the container so it can resolve its own dependencies.
|
|
9
|
+
*/
|
|
10
|
+
export class Container {
|
|
11
|
+
bindings = new Map();
|
|
12
|
+
instances = new Map();
|
|
13
|
+
/** Register a transient binding — a fresh value every resolve. */
|
|
14
|
+
bind(token, factory) {
|
|
15
|
+
this.bindings.set(token, { factory, shared: false });
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
/** Register a shared binding — resolved once, then cached. */
|
|
19
|
+
singleton(token, factory) {
|
|
20
|
+
this.bindings.set(token, { factory, shared: true });
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
/** Register an already-constructed value as a shared instance. */
|
|
24
|
+
instance(token, value) {
|
|
25
|
+
this.instances.set(token, value);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
/** True if the token is bound or has a cached instance. */
|
|
29
|
+
bound(token) {
|
|
30
|
+
return this.bindings.has(token) || this.instances.has(token);
|
|
31
|
+
}
|
|
32
|
+
/** Resolve a token out of the container. */
|
|
33
|
+
make(token) {
|
|
34
|
+
if (this.instances.has(token)) {
|
|
35
|
+
return this.instances.get(token);
|
|
36
|
+
}
|
|
37
|
+
const binding = this.bindings.get(token);
|
|
38
|
+
if (!binding) {
|
|
39
|
+
// Zero-arg classes can be auto-resolved without an explicit binding.
|
|
40
|
+
if (typeof token === "function") {
|
|
41
|
+
return this.build(token);
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Nothing bound in the container for [${String(token)}].`);
|
|
44
|
+
}
|
|
45
|
+
const resolved = binding.factory(this);
|
|
46
|
+
if (binding.shared) {
|
|
47
|
+
this.instances.set(token, resolved);
|
|
48
|
+
}
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
/** Instantiate a class, giving its constructor the container. */
|
|
52
|
+
build(ctor) {
|
|
53
|
+
return new ctor(this);
|
|
54
|
+
}
|
|
55
|
+
/** Alias `app(token)` sugar over `make`. */
|
|
56
|
+
get(token) {
|
|
57
|
+
return this.make(token);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP exceptions. Throw one of these anywhere in a handler, middleware, or
|
|
3
|
+
* service and the HTTP kernel turns it into the right response — a status code
|
|
4
|
+
* with a clean body (JSON or HTML), or a readable error page in debug mode.
|
|
5
|
+
*/
|
|
6
|
+
export declare const STATUS_TEXT: Record<number, string>;
|
|
7
|
+
/** A semantic HTTP error carrying a status code and optional headers. */
|
|
8
|
+
export declare class HttpException extends Error {
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly headers?: Record<string, string>;
|
|
11
|
+
constructor(status: number, message?: string, headers?: Record<string, string>);
|
|
12
|
+
}
|
|
13
|
+
export declare class NotFoundException extends HttpException {
|
|
14
|
+
constructor(message?: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class UnauthorizedException extends HttpException {
|
|
17
|
+
constructor(message?: string);
|
|
18
|
+
}
|
|
19
|
+
export declare class ForbiddenException extends HttpException {
|
|
20
|
+
constructor(message?: string);
|
|
21
|
+
}
|
|
22
|
+
/** 422 with per-field messages. Pairs with a future validation layer. */
|
|
23
|
+
export declare class ValidationException extends HttpException {
|
|
24
|
+
readonly errors: Record<string, string[]>;
|
|
25
|
+
constructor(errors: Record<string, string[]>, message?: string);
|
|
26
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP exceptions. Throw one of these anywhere in a handler, middleware, or
|
|
3
|
+
* service and the HTTP kernel turns it into the right response — a status code
|
|
4
|
+
* with a clean body (JSON or HTML), or a readable error page in debug mode.
|
|
5
|
+
*/
|
|
6
|
+
export const STATUS_TEXT = {
|
|
7
|
+
400: "Bad Request",
|
|
8
|
+
401: "Unauthorized",
|
|
9
|
+
403: "Forbidden",
|
|
10
|
+
404: "Not Found",
|
|
11
|
+
405: "Method Not Allowed",
|
|
12
|
+
409: "Conflict",
|
|
13
|
+
419: "Page Expired",
|
|
14
|
+
422: "Unprocessable Entity",
|
|
15
|
+
429: "Too Many Requests",
|
|
16
|
+
500: "Internal Server Error",
|
|
17
|
+
503: "Service Unavailable",
|
|
18
|
+
};
|
|
19
|
+
/** A semantic HTTP error carrying a status code and optional headers. */
|
|
20
|
+
export class HttpException extends Error {
|
|
21
|
+
status;
|
|
22
|
+
headers;
|
|
23
|
+
constructor(status, message, headers) {
|
|
24
|
+
super(message ?? STATUS_TEXT[status] ?? "Error");
|
|
25
|
+
this.name = "HttpException";
|
|
26
|
+
this.status = status;
|
|
27
|
+
this.headers = headers;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class NotFoundException extends HttpException {
|
|
31
|
+
constructor(message = "Not Found") {
|
|
32
|
+
super(404, message);
|
|
33
|
+
this.name = "NotFoundException";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class UnauthorizedException extends HttpException {
|
|
37
|
+
constructor(message = "Unauthorized") {
|
|
38
|
+
super(401, message);
|
|
39
|
+
this.name = "UnauthorizedException";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class ForbiddenException extends HttpException {
|
|
43
|
+
constructor(message = "Forbidden") {
|
|
44
|
+
super(403, message);
|
|
45
|
+
this.name = "ForbiddenException";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** 422 with per-field messages. Pairs with a future validation layer. */
|
|
49
|
+
export class ValidationException extends HttpException {
|
|
50
|
+
errors;
|
|
51
|
+
constructor(errors, message = "The given data was invalid.") {
|
|
52
|
+
super(422, message);
|
|
53
|
+
this.name = "ValidationException";
|
|
54
|
+
this.errors = errors;
|
|
55
|
+
}
|
|
56
|
+
}
|