@elysiumjs/runtime-node 0.0.0-stage → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,76 @@
1
+ # @elysiumjs/runtime-node
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#5](https://github.com/na2axl/elysiumjs/pull/5) [`557a3ca`](https://github.com/na2axl/elysiumjs/commit/557a3cab7af8053a8bab9944e492e5022d91aa82) Thanks [@na2axl](https://github.com/na2axl)! - Core runs outside Bun. Environment variables, timers, the HTTP server, process exit and shutdown signals now come from a `Runtime` set with the new `runtime` option of `@Application.register()` or with `Runtime.use()`. Runtimes ship as packages, each exporting `runtime`: `@elysiumjs/runtime-bun`, `@elysiumjs/runtime-node` and `@elysiumjs/runtime-workers` (which also exports `createRuntime({ env })`).
8
+
9
+ Breaking: an application needs a runtime (add `runtime` from `@elysiumjs/runtime-bun`), and `Redis` and `RedisModule` moved from core to the new `@elysiumjs/redis` package, which also accepts a `client` factory for runtimes without `Bun.RedisClient`.
10
+
11
+ Console colours are plain ANSI codes instead of `Bun.color`, `node:readline` is loaded only when a prompt runs, and core's dist is built for the `node` target so `node:` imports keep their prefix. `check:runtime` scripts in core and runtime-workers fail when their bundles touch the `Bun` global.
12
+
13
+ `create-elysium` gets a runtime step: it installs the chosen runtime package and sets it in the generated app. Bun is selectable today; Node.js and Cloudflare Workers are listed as coming later.
14
+
15
+ - [#13](https://github.com/na2axl/elysiumjs/pull/13) [`1ea417e`](https://github.com/na2axl/elysiumjs/commit/1ea417e3583250bc2f975d795f398b7ff736115d) Thanks [@na2axl](https://github.com/na2axl)! - Hardening of the runtimes, the container and the CLI, checked by a conformance suite that runs on Bun, Node.js and workerd.
16
+
17
+ Breaking changes:
18
+
19
+ - `Runtime` gains the required `argv()`, `write(stream, text)` and `terminal()` members: core reaches the host's arguments, output streams and terminal only through the runtime. Custom runtimes must implement them.
20
+ - The third parameter of `InteractsWithConsole.write` is now `'stdout' | 'stderr'`.
21
+ - The WebSocket options are narrowed to `idleTimeout`, `maxPayloadLength` and `perMessageDeflate`, and the `bun` WebSocket types are replaced by `ElysiumWebSocket`.
22
+ - Redis connections are `RedisConnection`s, a port any Redis client can implement, instead of `Bun.RedisClient`.
23
+ - `styx create @scope/name` creates the project in `name/`, and refuses project and module names with upper-case letters.
24
+ - Queue handlers no longer reject the batch: after the handlers ran, each message is acked or retried on its own. `@Workers.queue(name, { each: true })` receives one message at a time, so a failure retries only that message.
25
+
26
+ Fixes:
27
+
28
+ - Request-scoped services are destroyed when the request ends, also on failure, and an async `onInit` is awaited. A second dispose of a request scope waits for the one in progress, so a Worker's `waitUntil` covers an async `onDestroy`.
29
+ - A failed module or service `onInit` no longer breaks later requests: the boot is retried, and the module's event listeners are unwired.
30
+ - `@elysiumjs/runtime-node` serves through `node:http`, keeping the Host header authoritative.
31
+ - `styx create` validates names before creating files, ships `.gitignore`, declares the dependencies of created projects and imports their default controllers.
32
+
33
+ - [#14](https://github.com/na2axl/elysiumjs/pull/14) [`0956186`](https://github.com/na2axl/elysiumjs/commit/0956186db0ca5a83c0a6636aeb2a4c81a0033de4) Thanks [@na2axl](https://github.com/na2axl)! - The request pipeline: problem details, exception filters, interceptors, typed configuration, HTTP security options and an HTTP test client, checked on Bun, Node.js, workerd and AOT-compiled Workers.
34
+
35
+ Breaking changes:
36
+
37
+ - Error responses are RFC 9457 problem details (`application/problem+json`). Request validation errors answer 422 with an `errors` list of JSON pointers (`problems.validationStatus: 400` keeps 400). Server errors only show their message and stack in development. Thrown statuses (`c.status(...)`) are sent as they are.
38
+ - Client errors (4xx) are no longer reported to `onError`, the `elysium:error` event or the trace.
39
+ - `Runtime.serve` may return a handle with `close()`, which the application calls on shutdown.
40
+ - Request bodies are limited to 1 MiB by default (`http.bodyLimit`), also when chunked.
41
+ - The environment schema is validated when the app is built or run, with every error reported at once, instead of in the constructor.
42
+
43
+ Features:
44
+
45
+ - `DomainError`, with `status`, `type`, `title`, `detail` and extensions, and shortcuts such as `DomainError.notFound()`.
46
+ - Exception filters (`@Catch()`, `filters` on routes, controllers, modules and the app) and interceptors (`interceptors` on the same levels), with `TimingInterceptor`, `TransformInterceptor` and mnemosyne's `CacheInterceptor`.
47
+ - Typed configuration: `Secret` values, module `envSchema`s merged into the app's, `InvalidEnvironmentError`.
48
+ - `http` options: CORS, security headers, body limits and trusted proxies (`ctx.ip`). Runtimes report the client address through `clientAddress`.
49
+ - `createTestingHttpApp` and `createHttpClient` send requests to an app in memory, with `expectStatus()`.
50
+
51
+ Fixes:
52
+
53
+ - App-level `onRequest` middlewares run in the request scope.
54
+ - On Workers, the request scope of a streamed response is disposed when the body ends, not when `fetch` returns.
55
+ - `KeyvWorkersKV.clear()` stays within an operation budget (`maxOperations`).
56
+ - The Node.js bridge treats a destroyed socket as a client abort.
57
+ - Heracles performance metrics report through the Redis port (`connection` or `client`), not a Bun client.
58
+ - `styx routes:generate` names anonymous default controllers after their file; `styx create` pins `@elysiumjs/*` packages to the versions it was released with; invalid names are reported as Elysium errors.
59
+
60
+ ### Patch Changes
61
+
62
+ - [#7](https://github.com/na2axl/elysiumjs/pull/7) [`e9c3d98`](https://github.com/na2axl/elysiumjs/commit/e9c3d98dd2714b69100215f7c8a80046da463148) Thanks [@na2axl](https://github.com/na2axl)! - Elysium runs on Elysia 2 (`elysia@^2.0.0-rc.1`), with TypeBox 1 (`typebox`), `@elysia/openapi` and `@elysia/opentelemetry`.
63
+
64
+ Breaking:
65
+
66
+ - `TSchema` and `Static` are re-exported from `typebox` instead of `@sinclair/typebox`; `Value` comes from `typebox/value` and `Value.Errors()` returns an array.
67
+ - `Application.onError()` receives a `ServerError` (exported from core). Elysia now names its errors in kebab case; the default error response keeps the upper snake case names (`VALIDATION`, `NOT_FOUND`, `UNKNOWN` for other errors).
68
+ - `AppHttpContext` is a plain interface with `decorator`, `store` and `derive` records (Elysia 2 renamed `resolve` to `derive`).
69
+ - Websocket controllers install Elysia's `websocket()` capability themselves.
70
+
71
+ A module or controller `onRequest` middleware that answers now stops the request: Elysia 2 ignores values returned from a transform hook, so the response is thrown as a status and sent as is, without going through `onError`.
72
+
73
+ `mnemosyne` builds date columns with `t.Codec` (TypeBox 1 dropped `Transform`). `argus` traces every route of the app it is installed on and ends each span once, keeping the error status when the request failed.
74
+
75
+ - Updated dependencies [[`6bc48fe`](https://github.com/na2axl/elysiumjs/commit/6bc48febd51f7090c582e73f349eaeff7261241c), [`e9c3d98`](https://github.com/na2axl/elysiumjs/commit/e9c3d98dd2714b69100215f7c8a80046da463148), [`557a3ca`](https://github.com/na2axl/elysiumjs/commit/557a3cab7af8053a8bab9944e492e5022d91aa82), [`ccc3f5b`](https://github.com/na2axl/elysiumjs/commit/ccc3f5bb904d1bf52d8c72ebf7ea8d10f617a8c4), [`6789879`](https://github.com/na2axl/elysiumjs/commit/678987976ecfd46b534b65276e0de3f448977562), [`87299fd`](https://github.com/na2axl/elysiumjs/commit/87299fd9da82d1a408ced1891c0c3d42991444e5), [`bee66bd`](https://github.com/na2axl/elysiumjs/commit/bee66bdc874b8f23fea356f7d3adf7555a48ca64), [`0057b67`](https://github.com/na2axl/elysiumjs/commit/0057b67df0e3abecab8a9accff6b55a6a64c21df), [`632d77b`](https://github.com/na2axl/elysiumjs/commit/632d77b561fc43ade605fb69b06ac1b9b9a2ffc4), [`1ea417e`](https://github.com/na2axl/elysiumjs/commit/1ea417e3583250bc2f975d795f398b7ff736115d), [`0956186`](https://github.com/na2axl/elysiumjs/commit/0956186db0ca5a83c0a6636aeb2a4c81a0033de4), [`fbcc7da`](https://github.com/na2axl/elysiumjs/commit/fbcc7dad127db29da42e7a4a5c2124966731cd88), [`ac96e55`](https://github.com/na2axl/elysiumjs/commit/ac96e55874b0ffe350b287c4278cd85afecb7576), [`98a16aa`](https://github.com/na2axl/elysiumjs/commit/98a16aae889d46f7a9fb5573188f4cbe3629dd3b)]:
76
+ - @elysiumjs/core@0.11.0
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @elysiumjs/runtime-node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Node.js runtime for [Elysium.js](https://github.com/na2axl/elysiumjs).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @elysiumjs/runtime-node
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { Application } from '@elysiumjs/core';
15
+ import { runtime } from '@elysiumjs/runtime-node';
16
+
17
+ @Application.register({ runtime, modules: [] })
18
+ class App extends Application {}
19
+ ```
20
+
21
+ The package exports `runtime`; rename it at import if you like: `import { runtime as nodeRuntime } from '@elysiumjs/runtime-node'`.
22
+
23
+ Uses `process.env`, timers, `process.exit` and `SIGINT`/`SIGTERM` handlers. It serves the app on a `node:http` server through `serveNode`, a bridge to Elysia's `fetch` handler (no Elysia 2 Node adapter is released yet), so no adapter needs to be configured.
24
+
25
+ ## License
26
+
27
+ Apache-2.0
@@ -0,0 +1,9 @@
1
+ import type { Runtime } from '@elysiumjs/core';
2
+ export { type FetchHandler, peerAddress, serveNode } from './serve';
3
+ /**
4
+ * The Elysium runtime for Node.js. It serves the app's `fetch` handler on a `node:http` server
5
+ * through {@link serveNode}, a bridge used instead of an Elysia Node adapter because none is
6
+ * released for Elysia 2 yet (`@elysia/node` only has `2.0.0-exp.*` builds).
7
+ * @author Axel Nana <axel.nana@workbud.com>
8
+ */
9
+ export declare const runtime: Runtime;
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{createServer}from"node:http";import{Readable}from"node:stream";import{pipeline}from"node:stream/promises";import{getLogger}from"@elysiumjs/core";var BODYLESS_METHODS=new Set(["GET","HEAD"]),STATUS_BAD_REQUEST=400,STATUS_INTERNAL_ERROR=500,toHeaders=(raw)=>{let headers=new Headers;for(let i=0;i+1<raw.length;i+=2)headers.append(raw[i],raw[i+1]);return headers},pathOf=(target)=>{if(target.startsWith("/"))return target;try{let absolute=new URL(target);if(absolute.protocol==="http:"||absolute.protocol==="https:")return`${absolute.pathname}${absolute.search}`}catch{}throw TypeError(`Unsupported request-target: ${target}`)},toUrl=(req)=>{let protocol=req.socket.encrypted?"https":"http",host=req.headers.host??"localhost",origin;try{origin=new URL(`${protocol}://${host}`)}catch{throw TypeError(`Invalid Host header: ${host}`)}if(!(origin.pathname==="/"&&!origin.search&&!origin.hash)||origin.username||origin.password)throw TypeError(`Invalid Host header: ${host}`);return new URL(`${origin.origin}${pathOf(req.url??"/")}`)},peers=new WeakMap,peerAddress=(request)=>peers.get(request),toRequest=(req,url,signal)=>{let method=req.method??"GET",body=BODYLESS_METHODS.has(method)?{}:{body:Readable.toWeb(req),duplex:"half"},request=new Request(url,{method,headers:toHeaders(req.rawHeaders),signal,...body});if(req.socket.remoteAddress)peers.set(request,req.socket.remoteAddress);return request},writeResponse=async(response,res)=>{if(res.statusCode=response.status,response.statusText)res.statusMessage=response.statusText;for(let[name,value]of response.headers)if(name!=="set-cookie")res.setHeader(name,value);let cookies=response.headers.getSetCookie();if(cookies.length>0)res.setHeader("set-cookie",cookies);if(!response.body){res.end();return}await pipeline(Readable.fromWeb(response.body),res)},fail=(res,status)=>{if(res.headersSent){res.destroy();return}res.writeHead(status).end()},ABORT_ERROR_CODES=new Set(["ABORT_ERR","ERR_STREAM_PREMATURE_CLOSE"]),isAbortError=(error)=>error instanceof Error&&(error.name==="AbortError"||ABORT_ERROR_CODES.has(error.code??"")),handle=async(fetch,req,res)=>{let controller=new AbortController;res.once("close",()=>{if(!res.writableFinished)controller.abort()});let url;try{url=toUrl(req)}catch{fail(res,STATUS_BAD_REQUEST);return}try{await writeResponse(await fetch(toRequest(req,url,controller.signal)),res)}catch(error){if(controller.signal.aborted&&isAbortError(error)||res.destroyed||req.destroyed)return;getLogger({runtime:"node"}).error("Failed to serve a request",{method:req.method,url:url.href,error}),fail(res,STATUS_INTERNAL_ERROR)}},serveNode=(fetch,port)=>createServer((req,res)=>void handle(fetch,req,res)).listen(port);var SIGNALS=["SIGINT","SIGTERM"],runtime={name:"node",env:()=>process.env,sleep:(ms)=>new Promise((resolve)=>setTimeout(resolve,ms)),serve(app,port){let server=serveNode(async(request)=>app.fetch(request),port);return{close:()=>new Promise((resolve,reject)=>{server.close((error)=>error?reject(error):resolve()),server.closeIdleConnections()})}},clientAddress:peerAddress,exit(code){process.exit(code)},argv:()=>process.argv.slice(2),write(stream,text){process[stream].write(text)},terminal:()=>({input:process.stdin,output:process.stdout,setRawMode:(on)=>process.stdin.setRawMode?.(on),clear:()=>void process.stdout.write("\x1Bc"),moveCursor:(x,y)=>void process.stdout.moveCursor?.(x,y),clearLine:()=>{process.stdout.clearLine?.(0),process.stdout.cursorTo?.(0)}}),onShutdown(handler){for(let signal of SIGNALS)process.on(signal,handler);return()=>{for(let signal of SIGNALS)process.off(signal,handler)}}};export{peerAddress,runtime,serveNode};
@@ -0,0 +1,30 @@
1
+ import type { Server } from 'node:http';
2
+ /**
3
+ * A web-standard request handler, such as `Elysia.fetch`.
4
+ * @author Axel Nana <axel.nana@workbud.com>
5
+ */
6
+ export type FetchHandler = (request: Request) => Promise<Response>;
7
+ /**
8
+ * The address of the peer that sent a request served by {@link serveNode}.
9
+ * @author Axel Nana <axel.nana@workbud.com>
10
+ * @param request A request created by the bridge.
11
+ */
12
+ export declare const peerAddress: (request: Request) => string | undefined;
13
+ /**
14
+ * Serves a web-standard fetch handler on a `node:http` server. Request bodies stream from the
15
+ * socket (`Readable.toWeb`, `duplex: 'half'`), response bodies stream to it, status and headers
16
+ * are copied (every `set-cookie` header is kept), and the request `signal` aborts when the client
17
+ * disconnects before the response is complete.
18
+ *
19
+ * The `Host` header always names the request's host: an origin-form target such as `//x/y` is a
20
+ * path on that host, and an absolute-form target (`GET http://other/a`) only contributes its path
21
+ * and query. A `Host` header that is not a bare host, or a target that is neither form, gets a 400.
22
+ *
23
+ * This bridge exists because no Elysia 2 Node adapter is released yet (`@elysia/node` only has
24
+ * `2.0.0-exp.*` builds, and `@elysiajs/node` targets Elysia 1).
25
+ * @author Axel Nana <axel.nana@workbud.com>
26
+ * @param fetch The handler that answers each request, such as `(r) => app.fetch(r)`.
27
+ * @param port The port to listen on; `0` picks a free one (read it from `server.address()`).
28
+ * @returns The listening server, to close it.
29
+ */
30
+ export declare const serveNode: (fetch: FetchHandler, port: number) => Server;
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@elysiumjs/runtime-node",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "description": "Node.js runtime for the Elysium.js framework.",
6
+ "private": false,
7
+ "author": {
8
+ "name": "Axel Nana",
9
+ "email": "axel.nana@workbud.com"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/na2axl/elysiumjs.git",
14
+ "directory": "@elysiumjs/runtime-node"
15
+ },
16
+ "homepage": "https://github.com/na2axl/elysiumjs/tree/main/@elysiumjs/runtime-node#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/na2axl/elysiumjs/issues"
19
+ },
20
+ "scripts": {
21
+ "format": "prettier --write \"./src/**/*.ts\"",
22
+ "clean": "rimraf dist",
23
+ "build:js": "bun build ./src/index.ts --outdir ./dist --target node --packages external --root ./src --minify-syntax --minify-whitespace",
24
+ "build:ts": "tsc",
25
+ "build": "bun run clean && bun run build:js && bun run build:ts",
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "bun run build",
28
+ "test": "bun test",
29
+ "test:coverage": "bun test --coverage"
30
+ },
31
+ "keywords": [
32
+ "elysium",
33
+ "framework",
34
+ "typescript",
35
+ "runtime",
36
+ "node"
37
+ ],
38
+ "type": "module",
39
+ "module": "dist/index.js",
40
+ "types": "dist/index.d.ts",
41
+ "files": [
42
+ "dist",
43
+ "CHANGELOG.md"
44
+ ],
45
+ "sideEffects": false,
46
+ "devDependencies": {
47
+ "@elysiumjs/core": "0.11.0",
48
+ "elysia": "^2.0.0-rc.1"
49
+ },
50
+ "peerDependencies": {
51
+ "@elysiumjs/core": "^0.11.0"
52
+ }
53
+ }