@erenthedeveloper0/zen 0.1.0-alpha.1

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 ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eren Sümer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ <p align="center">
2
+ <img alt="zen.js — a compiler-first web framework" src="https://raw.githubusercontent.com/erenthedeveloper0/zen/main/.github/images/banner-dark.png" width="100%">
3
+ </p>
4
+
5
+ # @erenthedeveloper0/zen
6
+
7
+ **A compiler-first web framework for Node.js.** Express-simple, Fastify-fast,
8
+ typed end to end.
9
+
10
+ > **Alpha.** The API will change before `1.0`. Do not put this in production yet — see
11
+ > [the status section](https://github.com/erenthedeveloper0/zen#status).
12
+
13
+ ```bash
14
+ npm install @erenthedeveloper0/zen@alpha
15
+ ```
16
+
17
+ ```ts
18
+ import { zen } from '@erenthedeveloper0/zen'
19
+
20
+ const app = zen()
21
+
22
+ app.get('/', () => 'Hello world')
23
+
24
+ await app.listen(3000)
25
+ ```
26
+
27
+ Two concepts: `app.METHOD(path, handler)`, and **the handler returns the
28
+ response**. There is no response object to learn.
29
+
30
+ ## What you get
31
+
32
+ Registration is a source language: `ready()` freezes the application and
33
+ compiles the router, every route's pipeline, its validators and response
34
+ serializers, and the request context's own memory layout into generated
35
+ JavaScript. Stages a route does not use are not emitted at all.
36
+
37
+ - **Typed routes** from the path template and your schemas — Zod, Valibot and
38
+ ArkType work through [Standard Schema](https://standardschema.dev) with no
39
+ adapter.
40
+ - **Response contracts**: a route that declares `response: { 200: User }`
41
+ compiles a serializer that *cannot* emit a field `User` does not declare.
42
+ - **Deadlines**, **health and readiness endpoints**, **validated configuration**,
43
+ **content negotiation**, **server-sent events**, **file responses** with
44
+ `ETag`/`Range`, and **graceful shutdown** that drains before it refuses.
45
+ - **Boot-time diagnostics**, aggregated: every registration problem in one run,
46
+ each with a fix.
47
+
48
+ ```ts
49
+ import { zen } from '@erenthedeveloper0/zen'
50
+ import { z } from 'zod'
51
+
52
+ const app = zen({ timeout: '30s' })
53
+
54
+ app.get('/users/:id<int>', {
55
+ response: { 200: z.object({ id: z.number(), name: z.string() }) },
56
+ }, async (ctx) => {
57
+ const user = await db.users.find(ctx.params.id) // ctx.params.id is a number
58
+ return user // a passwordHash on it never reaches the wire
59
+ })
60
+
61
+ await app.listen() // address from config.server
62
+ ```
63
+
64
+ ## This package
65
+
66
+ `@erenthedeveloper0/zen` wires together:
67
+
68
+ | Package | Role |
69
+ | --- | --- |
70
+ | [`@erenthedeveloper0/zen-core`](https://www.npmjs.com/package/@erenthedeveloper0/zen-core) | registries, compilers, runtime, errors — zero dependencies |
71
+ | [`@erenthedeveloper0/zen-router`](https://www.npmjs.com/package/@erenthedeveloper0/zen-router) | the compiled radix router |
72
+ | [`@erenthedeveloper0/zen-adapter-node`](https://www.npmjs.com/package/@erenthedeveloper0/zen-adapter-node) | Node's `http` server |
73
+ | [`@erenthedeveloper0/zen-middleware`](https://www.npmjs.com/package/@erenthedeveloper0/zen-middleware) | CORS, security headers, request ids, rate limiting |
74
+
75
+ and re-exports all of them, so one import is enough. It also supplies the two
76
+ things only a Node process has: `process.env` as the configuration environment,
77
+ and signal handling — `SIGTERM`/`SIGINT` run the graceful shutdown and exit, and
78
+ an uncaught exception is logged and does the same with exit code 1. Pass
79
+ `lifecycle: false` if something else manages the process.
80
+
81
+ OpenAPI generation is a separate install:
82
+ [`@erenthedeveloper0/zen-openapi`](https://www.npmjs.com/package/@erenthedeveloper0/zen-openapi).
83
+
84
+ ## Requirements
85
+
86
+ - Node.js **≥ 22.6**
87
+ - TypeScript **≥ 5.0**, if you use TypeScript
88
+
89
+ ## Documentation
90
+
91
+ - [README](https://github.com/erenthedeveloper0/zen#readme) — the tour
92
+ - [ARCHITECTURE.md](https://github.com/erenthedeveloper0/zen/blob/main/ARCHITECTURE.md) — the design, and the arguments that lost
93
+ - [Error codes](https://github.com/erenthedeveloper0/zen/blob/main/docs/errors.md)
94
+ - [Examples](https://github.com/erenthedeveloper0/zen/tree/main/examples)
95
+
96
+ [MIT](https://github.com/erenthedeveloper0/zen/blob/main/LICENSE) © [Eren Sümer](https://github.com/erenthedeveloper0) · [contributors](https://github.com/erenthedeveloper0/zen/blob/main/CONTRIBUTORS.md)
@@ -0,0 +1,56 @@
1
+ import { ZenApp, type HostLifecycle, type ZenOptions } from '@erenthedeveloper0/zen-core';
2
+ /**
3
+ * The meta-package — rfcs/0001 §24.1.
4
+ *
5
+ * Beginners install one thing (`zen`); experts install six (`@erenthedeveloper0/zen-core`,
6
+ * `@erenthedeveloper0/zen-router`, an adapter, …). This module is the *only* place the three
7
+ * are wired together, which is what keeps `@erenthedeveloper0/zen-core` free of any router or
8
+ * platform dependency.
9
+ */
10
+ export type ZenAppOptions<C = Record<string, never>> = Partial<Omit<ZenOptions<C>, 'router' | 'pathParser' | 'lifecycle'>> & {
11
+ readonly router?: ZenOptions['router'] | undefined;
12
+ readonly pathParser?: ZenOptions['pathParser'] | undefined;
13
+ /**
14
+ * Signals and crash handling — §4.5, §12.8. Defaults to
15
+ * `processLifecycle()`: `SIGTERM`/`SIGINT` drain and exit, an uncaught
16
+ * error is logged and shuts down with exit 1. `false` leaves the process
17
+ * alone, for a host that manages it itself.
18
+ */
19
+ readonly lifecycle?: HostLifecycle | false | undefined;
20
+ };
21
+ /**
22
+ * The five-line app:
23
+ *
24
+ * import { zen } from '@erenthedeveloper0/zen'
25
+ * const app = zen()
26
+ * app.get('/', () => 'Hello world')
27
+ * app.listen({ port: 3000 })
28
+ *
29
+ * Two concepts — `app.METHOD(path, handler)`, and *the handler returns the
30
+ * response*. That is one fewer than Express, because there is no response
31
+ * object to learn (§1.2).
32
+ */
33
+ export declare function zen<X = {}, C = Record<string, never>>(options?: ZenAppOptions<C>): ZenApp<X & {
34
+ readonly config: C;
35
+ }>;
36
+ export default zen;
37
+ export * from '@erenthedeveloper0/zen-core';
38
+ export { ZenRouter, parsePath, renderPath, BUILTIN_PARAM_TYPES, analyzeRoutes } from '@erenthedeveloper0/zen-router';
39
+ export { nodeAdapter, NODE_CAPABILITIES, mediaTypeFor, type NodeAdapterOptions } from '@erenthedeveloper0/zen-adapter-node';
40
+ export { processLifecycle, type ProcessLifecycleOptions } from './lifecycle.ts';
41
+ /**
42
+ * The first-party middleware pack — §24.2's "common middleware" row.
43
+ *
44
+ * Re-exported here and importable from `@erenthedeveloper0/zen-middleware` directly; §21.2's
45
+ * example uses the second form and `examples/middleware` follows it, so both
46
+ * paths stay exercised.
47
+ *
48
+ * Adding this row is what turned `@erenthedeveloper0/zen-core`'s `requestId` — the ULID
49
+ * generator, exported since 0.1 and imported by nothing outside core — into a
50
+ * live collision with the plugin of the same name, where `app.use(requestId())`
51
+ * would have registered a *string* as middleware and failed at compile with a
52
+ * message about neither. The generator is now `generateRequestId`, which is
53
+ * what it always was.
54
+ */
55
+ export * from '@erenthedeveloper0/zen-middleware';
56
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAKpG;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IACjD,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC,CAAC,GAAG;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAA;IAClD,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,YAAY,CAAC,GAAG,SAAS,CAAA;IAC1D;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,GAAG,KAAK,GAAG,SAAS,CAAA;CACvD,CAAA;AA4BH;;;;;;;;;;;GAWG;AACH,wBAAgB,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACnD,OAAO,GAAE,aAAa,CAAC,CAAC,CAAM,GAC7B,MAAM,CAAC,CAAC,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,CAapC;AAED,eAAe,GAAG,CAAA;AAGlB,cAAc,6BAA6B,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AACpH,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,qCAAqC,CAAA;AAC3H,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAE/E;;;;;;;;;;;;;GAaG;AACH,cAAc,mCAAmC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ import { createApp, ZenApp } from '@erenthedeveloper0/zen-core';
2
+ import { ZenRouter, parsePath } from '@erenthedeveloper0/zen-router';
3
+ import { nodeAdapter } from '@erenthedeveloper0/zen-adapter-node';
4
+ import { processLifecycle } from "./lifecycle.js";
5
+ /**
6
+ * The process environment, or nothing — rfcs/0001 §16.1 layer 6.
7
+ *
8
+ * This is the one line in the project that reaches for `process`, and it is
9
+ * here rather than in `@erenthedeveloper0/zen-core` on purpose: `process` does not exist on
10
+ * workerd, where the environment arrives as an argument to the fetch handler,
11
+ * so a core that read it would be a core that cannot run there (§3.3 B2). The
12
+ * meta-package already knows it is on Node — it imports the Node adapter — so
13
+ * it is the right place to know where the environment lives, and an app that
14
+ * wants a different source passes `env:` explicitly.
15
+ *
16
+ * Read through `globalThis` rather than the bare identifier so that a runtime
17
+ * without it produces `{}` instead of a `ReferenceError` on import.
18
+ */
19
+ function processEnv() {
20
+ const global = globalThis;
21
+ return global.process?.env ?? {};
22
+ }
23
+ const defaultPathParser = {
24
+ parse(path) {
25
+ const parsed = parsePath(path);
26
+ return { path: parsed.path, segments: parsed.segments };
27
+ },
28
+ };
29
+ /**
30
+ * The five-line app:
31
+ *
32
+ * import { zen } from '@erenthedeveloper0/zen'
33
+ * const app = zen()
34
+ * app.get('/', () => 'Hello world')
35
+ * app.listen({ port: 3000 })
36
+ *
37
+ * Two concepts — `app.METHOD(path, handler)`, and *the handler returns the
38
+ * response*. That is one fewer than Express, because there is no response
39
+ * object to learn (§1.2).
40
+ */
41
+ export function zen(options = {}) {
42
+ return createApp({
43
+ ...options,
44
+ router: options.router ?? new ZenRouter(),
45
+ pathParser: options.pathParser ?? defaultPathParser,
46
+ adapter: options.adapter ?? nodeAdapter(),
47
+ // §16.1 layer 6. Explicit `env` — including a list of `.env` sources —
48
+ // always wins; this is only the default nobody should have to write.
49
+ env: options.env ?? processEnv(),
50
+ // §4.5, §12.8 — installed at `listen()`, so an app that is only ever
51
+ // `inject()`ed in a test never touches the process.
52
+ lifecycle: options.lifecycle === false ? undefined : (options.lifecycle ?? processLifecycle()),
53
+ });
54
+ }
55
+ export default zen;
56
+ // Re-export the full public surface so `import { … } from '@erenthedeveloper0/zen'` is enough.
57
+ export * from '@erenthedeveloper0/zen-core';
58
+ export { ZenRouter, parsePath, renderPath, BUILTIN_PARAM_TYPES, analyzeRoutes } from '@erenthedeveloper0/zen-router';
59
+ export { nodeAdapter, NODE_CAPABILITIES, mediaTypeFor } from '@erenthedeveloper0/zen-adapter-node';
60
+ export { processLifecycle } from "./lifecycle.js";
61
+ /**
62
+ * The first-party middleware pack — §24.2's "common middleware" row.
63
+ *
64
+ * Re-exported here and importable from `@erenthedeveloper0/zen-middleware` directly; §21.2's
65
+ * example uses the second form and `examples/middleware` follows it, so both
66
+ * paths stay exercised.
67
+ *
68
+ * Adding this row is what turned `@erenthedeveloper0/zen-core`'s `requestId` — the ULID
69
+ * generator, exported since 0.1 and imported by nothing outside core — into a
70
+ * live collision with the plugin of the same name, where `app.use(requestId())`
71
+ * would have registered a *string* as middleware and failed at compile with a
72
+ * message about neither. The generator is now `generateRequestId`, which is
73
+ * what it always was.
74
+ */
75
+ export * from '@erenthedeveloper0/zen-middleware';
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAuC,MAAM,6BAA6B,CAAA;AACpG,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAA;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAuBjD;;;;;;;;;;;;;GAaG;AACH,SAAS,UAAU;IACjB,MAAM,MAAM,GAAG,UAAwE,CAAA;IACvF,OAAO,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,EAAE,CAAA;AAClC,CAAC;AAED,MAAM,iBAAiB,GAA6B;IAClD,KAAK,CAAC,IAAY;QAChB,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;QAC9B,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAA;IACzD,CAAC;CACF,CAAA;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,GAAG,CACjB,UAA4B,EAAE;IAE9B,OAAO,SAAS,CAAO;QACrB,GAAG,OAAO;QACV,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI,SAAS,EAAE;QACzC,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,iBAAiB;QACnD,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,WAAW,EAAE;QACzC,uEAAuE;QACvE,qEAAqE;QACrE,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE;QAChC,qEAAqE;QACrE,oDAAoD;QACpD,SAAS,EAAE,OAAO,CAAC,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,IAAI,gBAAgB,EAAE,CAAC;KAC/F,CAAC,CAAA;AACJ,CAAC;AAED,eAAe,GAAG,CAAA;AAElB,+FAA+F;AAC/F,cAAc,6BAA6B,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AACpH,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAA2B,MAAM,qCAAqC,CAAA;AAC3H,OAAO,EAAE,gBAAgB,EAAgC,MAAM,gBAAgB,CAAA;AAE/E;;;;;;;;;;;;;GAaG;AACH,cAAc,mCAAmC,CAAA"}
@@ -0,0 +1,45 @@
1
+ import type { HostLifecycle } from '@erenthedeveloper0/zen-core';
2
+ export interface ProcessLifecycleOptions {
3
+ /**
4
+ * Signals that start a graceful shutdown. `SIGTERM` is what Kubernetes, ECS,
5
+ * systemd and `docker stop` send; `SIGINT` is Ctrl+C.
6
+ */
7
+ readonly signals?: readonly string[] | undefined;
8
+ /**
9
+ * Exit once shutdown finishes — 0 after a signal, 1 after a crash. On by
10
+ * default, because a process whose server has closed and whose pools are
11
+ * disposed has nothing left to do, and one that lingers holds its container.
12
+ */
13
+ readonly exit?: boolean | undefined;
14
+ /**
15
+ * §12.8: on an uncaught exception or unhandled rejection, log it at `fatal`
16
+ * and shut down with exit code 1. On by default.
17
+ */
18
+ readonly crashes?: boolean | undefined;
19
+ /** Called when a shutdown signal arrives, before anything closes. */
20
+ readonly onSignal?: ((signal: string) => void) | undefined;
21
+ }
22
+ /**
23
+ * The Node process's side of the lifecycle — rfcs/0001 §4.5, §12.8.
24
+ *
25
+ * `zen()` installs this by default when the app starts listening. Before it
26
+ * existed, §4.5 described what happens "on SIGTERM" and nothing listened for
27
+ * one: Node's default action ended the process on the spot, so readiness never
28
+ * went red, the drain window never opened, and `onClose` never ran — on every
29
+ * rolling deploy, which is the exact situation §4.5 exists for. Every example
30
+ * in this repository wrote the same four lines to fill the gap, and two did
31
+ * not.
32
+ *
33
+ * - **First signal:** run `app.close()` — readiness drains, the socket
34
+ * closes, in-flight requests finish, `onClose` runs, services dispose — then
35
+ * exit 0.
36
+ * - **Second signal while that is in progress:** exit now. Ctrl+C twice
37
+ * means "stop waiting", and a shutdown stuck behind a hung connection
38
+ * should not be un-killable from a terminal.
39
+ * - **An uncaught exception or unhandled rejection:** log it at `fatal`, then
40
+ * the same graceful shutdown with exit 1. Zen does not keep serving from a
41
+ * process whose state is unknown (§12.8) — that instinct is how corrupted
42
+ * data gets written — but it does let requests already in flight finish.
43
+ */
44
+ export declare function processLifecycle(options?: ProcessLifecycleOptions): HostLifecycle;
45
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAE7E,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;IAChD;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACnC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACtC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CAC3D;AAID;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,aAAa,CAwDrF"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The Node process's side of the lifecycle — rfcs/0001 §4.5, §12.8.
3
+ *
4
+ * `zen()` installs this by default when the app starts listening. Before it
5
+ * existed, §4.5 described what happens "on SIGTERM" and nothing listened for
6
+ * one: Node's default action ended the process on the spot, so readiness never
7
+ * went red, the drain window never opened, and `onClose` never ran — on every
8
+ * rolling deploy, which is the exact situation §4.5 exists for. Every example
9
+ * in this repository wrote the same four lines to fill the gap, and two did
10
+ * not.
11
+ *
12
+ * - **First signal:** run `app.close()` — readiness drains, the socket
13
+ * closes, in-flight requests finish, `onClose` runs, services dispose — then
14
+ * exit 0.
15
+ * - **Second signal while that is in progress:** exit now. Ctrl+C twice
16
+ * means "stop waiting", and a shutdown stuck behind a hung connection
17
+ * should not be un-killable from a terminal.
18
+ * - **An uncaught exception or unhandled rejection:** log it at `fatal`, then
19
+ * the same graceful shutdown with exit 1. Zen does not keep serving from a
20
+ * process whose state is unknown (§12.8) — that instinct is how corrupted
21
+ * data gets written — but it does let requests already in flight finish.
22
+ */
23
+ export function processLifecycle(options = {}) {
24
+ const signals = options.signals ?? ['SIGTERM', 'SIGINT'];
25
+ const exit = options.exit ?? true;
26
+ const crashes = options.crashes ?? true;
27
+ return {
28
+ install(control) {
29
+ const proc = globalThis.process;
30
+ if (proc === undefined || typeof proc.on !== 'function')
31
+ return () => { };
32
+ let stopping = false;
33
+ const shutdown = (reason, code) => {
34
+ if (stopping) {
35
+ control.log.warn({ reason }, 'second shutdown request while shutting down; exiting now');
36
+ proc.exit(code === 0 ? 130 : code);
37
+ return;
38
+ }
39
+ stopping = true;
40
+ control.close(reason).then(() => { if (exit)
41
+ proc.exit(code); }, (error) => {
42
+ control.log.fatal({ err: error }, 'shutdown failed');
43
+ proc.exit(1);
44
+ });
45
+ };
46
+ const onSignal = (signal) => {
47
+ options.onSignal?.(signal);
48
+ shutdown(signal, 0);
49
+ };
50
+ const onException = (error) => {
51
+ control.log.fatal({ err: error }, 'uncaught exception; shutting down');
52
+ shutdown('uncaughtException', 1);
53
+ };
54
+ const onRejection = (reason) => {
55
+ control.log.fatal({ err: reason }, 'unhandled promise rejection; shutting down');
56
+ shutdown('unhandledRejection', 1);
57
+ };
58
+ for (const signal of signals)
59
+ proc.on(signal, onSignal);
60
+ if (crashes) {
61
+ proc.on('uncaughtException', onException);
62
+ proc.on('unhandledRejection', onRejection);
63
+ }
64
+ return () => {
65
+ for (const signal of signals)
66
+ proc.off(signal, onSignal);
67
+ if (crashes) {
68
+ proc.off('uncaughtException', onException);
69
+ proc.off('unhandledRejection', onRejection);
70
+ }
71
+ };
72
+ },
73
+ };
74
+ }
75
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAyBA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAmC,EAAE;IACpE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;IACxD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,IAAI,CAAA;IACjC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,IAAI,CAAA;IAEvC,OAAO;QACL,OAAO,CAAC,OAAoB;YAC1B,MAAM,IAAI,GAAI,UAA2C,CAAC,OAAO,CAAA;YACjE,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,EAAE,KAAK,UAAU;gBAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;YAExE,IAAI,QAAQ,GAAG,KAAK,CAAA;YAEpB,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAE,IAAY,EAAQ,EAAE;gBACtD,IAAI,QAAQ,EAAE,CAAC;oBACb,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,0DAA0D,CAAC,CAAA;oBACxF,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;oBAClC,OAAM;gBACR,CAAC;gBACD,QAAQ,GAAG,IAAI,CAAA;gBACf,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CACxB,GAAG,EAAE,GAAG,IAAI,IAAI;oBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,CAAC,EACnC,CAAC,KAAc,EAAE,EAAE;oBACjB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,iBAAiB,CAAC,CAAA;oBACpD,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;gBACd,CAAC,CACF,CAAA;YACH,CAAC,CAAA;YAED,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAQ,EAAE;gBACxC,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAA;gBAC1B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;YACrB,CAAC,CAAA;YACD,MAAM,WAAW,GAAG,CAAC,KAAc,EAAQ,EAAE;gBAC3C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,mCAAmC,CAAC,CAAA;gBACtE,QAAQ,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAA;YAClC,CAAC,CAAA;YACD,MAAM,WAAW,GAAG,CAAC,MAAe,EAAQ,EAAE;gBAC5C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,4CAA4C,CAAC,CAAA;gBAChF,QAAQ,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAA;YACnC,CAAC,CAAA;YAED,KAAK,MAAM,MAAM,IAAI,OAAO;gBAAE,IAAI,CAAC,EAAE,CAAC,MAAgB,EAAE,QAAQ,CAAC,CAAA;YACjE,IAAI,OAAO,EAAE,CAAC;gBACZ,IAAI,CAAC,EAAE,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAA;gBACzC,IAAI,CAAC,EAAE,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAA;YAC5C,CAAC;YAED,OAAO,GAAG,EAAE;gBACV,KAAK,MAAM,MAAM,IAAI,OAAO;oBAAE,IAAI,CAAC,GAAG,CAAC,MAAgB,EAAE,QAAQ,CAAC,CAAA;gBAClE,IAAI,OAAO,EAAE,CAAC;oBACZ,IAAI,CAAC,GAAG,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAA;oBAC1C,IAAI,CAAC,GAAG,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAA;gBAC7C,CAAC;YACH,CAAC,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@erenthedeveloper0/zen",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Zen: a compiler-first web framework for Node.js. Routing, validation, serialization and the request context are compiled at boot.",
5
+ "keywords": [
6
+ "web",
7
+ "framework",
8
+ "http",
9
+ "server",
10
+ "api",
11
+ "rest",
12
+ "typescript",
13
+ "openapi",
14
+ "validation",
15
+ "compiler",
16
+ "zen"
17
+ ],
18
+ "homepage": "https://github.com/erenthedeveloper0/zen/tree/main/packages/zen#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/erenthedeveloper0/zen/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/erenthedeveloper0/zen.git",
25
+ "directory": "packages/zen"
26
+ },
27
+ "license": "MIT",
28
+ "author": {
29
+ "name": "Eren Sümer",
30
+ "url": "https://github.com/erenthedeveloper0"
31
+ },
32
+ "type": "module",
33
+ "sideEffects": false,
34
+ "main": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "default": "./dist/index.js"
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "src",
46
+ "README.md",
47
+ "LICENSE"
48
+ ],
49
+ "engines": {
50
+ "node": ">=22.6.0"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc -b",
57
+ "test": "node --test \"test/**/*.test.ts\""
58
+ },
59
+ "dependencies": {
60
+ "@erenthedeveloper0/zen-adapter-node": "0.1.0-alpha.1",
61
+ "@erenthedeveloper0/zen-core": "0.1.0-alpha.1",
62
+ "@erenthedeveloper0/zen-middleware": "0.1.0-alpha.1",
63
+ "@erenthedeveloper0/zen-router": "0.1.0-alpha.1"
64
+ },
65
+ "peerDependencies": {
66
+ "typescript": ">=5.0"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "typescript": {
70
+ "optional": true
71
+ }
72
+ }
73
+ }
package/src/index.ts ADDED
@@ -0,0 +1,104 @@
1
+ import { createApp, ZenApp, type HostLifecycle, type ZenOptions } from '@erenthedeveloper0/zen-core'
2
+ import { ZenRouter, parsePath } from '@erenthedeveloper0/zen-router'
3
+ import { nodeAdapter } from '@erenthedeveloper0/zen-adapter-node'
4
+ import { processLifecycle } from './lifecycle.ts'
5
+
6
+ /**
7
+ * The meta-package — rfcs/0001 §24.1.
8
+ *
9
+ * Beginners install one thing (`zen`); experts install six (`@erenthedeveloper0/zen-core`,
10
+ * `@erenthedeveloper0/zen-router`, an adapter, …). This module is the *only* place the three
11
+ * are wired together, which is what keeps `@erenthedeveloper0/zen-core` free of any router or
12
+ * platform dependency.
13
+ */
14
+ export type ZenAppOptions<C = Record<string, never>> =
15
+ Partial<Omit<ZenOptions<C>, 'router' | 'pathParser' | 'lifecycle'>> & {
16
+ readonly router?: ZenOptions['router'] | undefined
17
+ readonly pathParser?: ZenOptions['pathParser'] | undefined
18
+ /**
19
+ * Signals and crash handling — §4.5, §12.8. Defaults to
20
+ * `processLifecycle()`: `SIGTERM`/`SIGINT` drain and exit, an uncaught
21
+ * error is logged and shuts down with exit 1. `false` leaves the process
22
+ * alone, for a host that manages it itself.
23
+ */
24
+ readonly lifecycle?: HostLifecycle | false | undefined
25
+ }
26
+
27
+ /**
28
+ * The process environment, or nothing — rfcs/0001 §16.1 layer 6.
29
+ *
30
+ * This is the one line in the project that reaches for `process`, and it is
31
+ * here rather than in `@erenthedeveloper0/zen-core` on purpose: `process` does not exist on
32
+ * workerd, where the environment arrives as an argument to the fetch handler,
33
+ * so a core that read it would be a core that cannot run there (§3.3 B2). The
34
+ * meta-package already knows it is on Node — it imports the Node adapter — so
35
+ * it is the right place to know where the environment lives, and an app that
36
+ * wants a different source passes `env:` explicitly.
37
+ *
38
+ * Read through `globalThis` rather than the bare identifier so that a runtime
39
+ * without it produces `{}` instead of a `ReferenceError` on import.
40
+ */
41
+ function processEnv(): Readonly<Record<string, string | undefined>> {
42
+ const global = globalThis as { process?: { env?: Record<string, string | undefined> } }
43
+ return global.process?.env ?? {}
44
+ }
45
+
46
+ const defaultPathParser: ZenOptions['pathParser'] = {
47
+ parse(path: string) {
48
+ const parsed = parsePath(path)
49
+ return { path: parsed.path, segments: parsed.segments }
50
+ },
51
+ }
52
+
53
+ /**
54
+ * The five-line app:
55
+ *
56
+ * import { zen } from '@erenthedeveloper0/zen'
57
+ * const app = zen()
58
+ * app.get('/', () => 'Hello world')
59
+ * app.listen({ port: 3000 })
60
+ *
61
+ * Two concepts — `app.METHOD(path, handler)`, and *the handler returns the
62
+ * response*. That is one fewer than Express, because there is no response
63
+ * object to learn (§1.2).
64
+ */
65
+ export function zen<X = {}, C = Record<string, never>>(
66
+ options: ZenAppOptions<C> = {},
67
+ ): ZenApp<X & { readonly config: C }> {
68
+ return createApp<X, C>({
69
+ ...options,
70
+ router: options.router ?? new ZenRouter(),
71
+ pathParser: options.pathParser ?? defaultPathParser,
72
+ adapter: options.adapter ?? nodeAdapter(),
73
+ // §16.1 layer 6. Explicit `env` — including a list of `.env` sources —
74
+ // always wins; this is only the default nobody should have to write.
75
+ env: options.env ?? processEnv(),
76
+ // §4.5, §12.8 — installed at `listen()`, so an app that is only ever
77
+ // `inject()`ed in a test never touches the process.
78
+ lifecycle: options.lifecycle === false ? undefined : (options.lifecycle ?? processLifecycle()),
79
+ })
80
+ }
81
+
82
+ export default zen
83
+
84
+ // Re-export the full public surface so `import { … } from '@erenthedeveloper0/zen'` is enough.
85
+ export * from '@erenthedeveloper0/zen-core'
86
+ export { ZenRouter, parsePath, renderPath, BUILTIN_PARAM_TYPES, analyzeRoutes } from '@erenthedeveloper0/zen-router'
87
+ export { nodeAdapter, NODE_CAPABILITIES, mediaTypeFor, type NodeAdapterOptions } from '@erenthedeveloper0/zen-adapter-node'
88
+ export { processLifecycle, type ProcessLifecycleOptions } from './lifecycle.ts'
89
+
90
+ /**
91
+ * The first-party middleware pack — §24.2's "common middleware" row.
92
+ *
93
+ * Re-exported here and importable from `@erenthedeveloper0/zen-middleware` directly; §21.2's
94
+ * example uses the second form and `examples/middleware` follows it, so both
95
+ * paths stay exercised.
96
+ *
97
+ * Adding this row is what turned `@erenthedeveloper0/zen-core`'s `requestId` — the ULID
98
+ * generator, exported since 0.1 and imported by nothing outside core — into a
99
+ * live collision with the plugin of the same name, where `app.use(requestId())`
100
+ * would have registered a *string* as middleware and failed at compile with a
101
+ * message about neither. The generator is now `generateRequestId`, which is
102
+ * what it always was.
103
+ */
104
+ export * from '@erenthedeveloper0/zen-middleware'
@@ -0,0 +1,104 @@
1
+ import type { HostControl, HostLifecycle } from '@erenthedeveloper0/zen-core'
2
+
3
+ export interface ProcessLifecycleOptions {
4
+ /**
5
+ * Signals that start a graceful shutdown. `SIGTERM` is what Kubernetes, ECS,
6
+ * systemd and `docker stop` send; `SIGINT` is Ctrl+C.
7
+ */
8
+ readonly signals?: readonly string[] | undefined
9
+ /**
10
+ * Exit once shutdown finishes — 0 after a signal, 1 after a crash. On by
11
+ * default, because a process whose server has closed and whose pools are
12
+ * disposed has nothing left to do, and one that lingers holds its container.
13
+ */
14
+ readonly exit?: boolean | undefined
15
+ /**
16
+ * §12.8: on an uncaught exception or unhandled rejection, log it at `fatal`
17
+ * and shut down with exit code 1. On by default.
18
+ */
19
+ readonly crashes?: boolean | undefined
20
+ /** Called when a shutdown signal arrives, before anything closes. */
21
+ readonly onSignal?: ((signal: string) => void) | undefined
22
+ }
23
+
24
+ type Signal = Parameters<NodeJS.Process['on']>[0]
25
+
26
+ /**
27
+ * The Node process's side of the lifecycle — rfcs/0001 §4.5, §12.8.
28
+ *
29
+ * `zen()` installs this by default when the app starts listening. Before it
30
+ * existed, §4.5 described what happens "on SIGTERM" and nothing listened for
31
+ * one: Node's default action ended the process on the spot, so readiness never
32
+ * went red, the drain window never opened, and `onClose` never ran — on every
33
+ * rolling deploy, which is the exact situation §4.5 exists for. Every example
34
+ * in this repository wrote the same four lines to fill the gap, and two did
35
+ * not.
36
+ *
37
+ * - **First signal:** run `app.close()` — readiness drains, the socket
38
+ * closes, in-flight requests finish, `onClose` runs, services dispose — then
39
+ * exit 0.
40
+ * - **Second signal while that is in progress:** exit now. Ctrl+C twice
41
+ * means "stop waiting", and a shutdown stuck behind a hung connection
42
+ * should not be un-killable from a terminal.
43
+ * - **An uncaught exception or unhandled rejection:** log it at `fatal`, then
44
+ * the same graceful shutdown with exit 1. Zen does not keep serving from a
45
+ * process whose state is unknown (§12.8) — that instinct is how corrupted
46
+ * data gets written — but it does let requests already in flight finish.
47
+ */
48
+ export function processLifecycle(options: ProcessLifecycleOptions = {}): HostLifecycle {
49
+ const signals = options.signals ?? ['SIGTERM', 'SIGINT']
50
+ const exit = options.exit ?? true
51
+ const crashes = options.crashes ?? true
52
+
53
+ return {
54
+ install(control: HostControl): () => void {
55
+ const proc = (globalThis as { process?: NodeJS.Process }).process
56
+ if (proc === undefined || typeof proc.on !== 'function') return () => {}
57
+
58
+ let stopping = false
59
+
60
+ const shutdown = (reason: string, code: number): void => {
61
+ if (stopping) {
62
+ control.log.warn({ reason }, 'second shutdown request while shutting down; exiting now')
63
+ proc.exit(code === 0 ? 130 : code)
64
+ return
65
+ }
66
+ stopping = true
67
+ control.close(reason).then(
68
+ () => { if (exit) proc.exit(code) },
69
+ (error: unknown) => {
70
+ control.log.fatal({ err: error }, 'shutdown failed')
71
+ proc.exit(1)
72
+ },
73
+ )
74
+ }
75
+
76
+ const onSignal = (signal: string): void => {
77
+ options.onSignal?.(signal)
78
+ shutdown(signal, 0)
79
+ }
80
+ const onException = (error: unknown): void => {
81
+ control.log.fatal({ err: error }, 'uncaught exception; shutting down')
82
+ shutdown('uncaughtException', 1)
83
+ }
84
+ const onRejection = (reason: unknown): void => {
85
+ control.log.fatal({ err: reason }, 'unhandled promise rejection; shutting down')
86
+ shutdown('unhandledRejection', 1)
87
+ }
88
+
89
+ for (const signal of signals) proc.on(signal as Signal, onSignal)
90
+ if (crashes) {
91
+ proc.on('uncaughtException', onException)
92
+ proc.on('unhandledRejection', onRejection)
93
+ }
94
+
95
+ return () => {
96
+ for (const signal of signals) proc.off(signal as Signal, onSignal)
97
+ if (crashes) {
98
+ proc.off('uncaughtException', onException)
99
+ proc.off('unhandledRejection', onRejection)
100
+ }
101
+ }
102
+ },
103
+ }
104
+ }