Init: RoggioApp Architecture, Prisma Schema, API MVP

This commit is contained in:
Clara Zetkin
2026-04-26 19:42:42 +02:00
commit 193b29e8a9
5256 changed files with 1446953 additions and 0 deletions
Generated Vendored Executable
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 - present, Yusuke Wada and Hono contributors
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
Generated Vendored Executable
+396
View File
@@ -0,0 +1,396 @@
# Node.js Adapter for Hono
This adapter `@hono/node-server` allows you to run your Hono application on Node.js.
Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js. It utilizes web standard APIs implemented in Node.js.
## Benchmarks
Hono is 4.1 times faster than Express.
Express:
```txt
$ bombardier -d 10s --fasthttp http://localhost:3000/
Statistics Avg Stdev Max
Reqs/sec 20803.37 1713.06 24910.85
Latency 6.01ms 5.21ms 451.37ms
HTTP codes:
1xx - 0, 2xx - 208131, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 5.75MB/s
```
Hono + `@hono/node-server`:
```txt
$ bombardier -d 10s --fasthttp http://localhost:3000/
Statistics Avg Stdev Max
Reqs/sec 85405.51 7250.65 102658.51
Latency 1.46ms 1.00ms 149.95ms
HTTP codes:
1xx - 0, 2xx - 854120, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 18.49MB/s
```
## Requirements
It works on Node.js versions greater than 20.x.
## Installation
You can install it from the npm registry with `npm` command:
```sh
npm install @hono/node-server
```
Or use `yarn`:
```sh
yarn add @hono/node-server
```
## Usage
Just import `@hono/node-server` at the top and write the code as usual.
The same code that runs on Cloudflare Workers, Deno, and Bun will work.
```ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hono meets Node.js'))
serve(app, (info) => {
console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000
})
```
## WebSocket
You can upgrade WebSocket connections with `upgradeWebSocket` from `@hono/node-server`.
To enable this, install `ws` (and `@types/ws`) in your project, then create and provide a `WebSocketServer` as shown in the example below.
```ts
import { serve, upgradeWebSocket } from '@hono/node-server'
import { WebSocketServer } from 'ws'
import { Hono } from 'hono'
const app = new Hono()
app.get(
'/ws',
upgradeWebSocket(() => ({
onMessage(event, ws) {
ws.send(event.data)
},
}))
)
const wss = new WebSocketServer({ noServer: true }) // important to create with `noServer: true`
serve({
fetch: app.fetch,
websocket: { server: wss },
})
```
For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`.
```sh
ts-node ./index.ts
```
Open `http://localhost:3000` with your browser.
## Options
### `port`
```ts
serve({
fetch: app.fetch,
port: 8787, // Port number, default is 3000
})
```
### `createServer`
```ts
import { createServer } from 'node:https'
import fs from 'node:fs'
//...
serve({
fetch: app.fetch,
createServer: createServer,
serverOptions: {
key: fs.readFileSync('test/fixtures/keys/agent1-key.pem'),
cert: fs.readFileSync('test/fixtures/keys/agent1-cert.pem'),
},
})
```
### `overrideGlobalObjects`
The default value is `true`. The Node.js Adapter rewrites the global Request/Response and uses a lightweight Request/Response to improve performance. If you don't want to do that, set `false`.
```ts
serve({
fetch: app.fetch,
overrideGlobalObjects: false,
})
```
### `autoCleanupIncoming`
The default value is `true`. The Node.js Adapter automatically cleans up (explicitly call `destroy()` method) if application is not finished to consume the incoming request. If you don't want to do that, set `false`.
If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment and there is no process that returns a response without reading the body of the POST request all the way through, you can improve performance by setting it to `false`.
```ts
serve({
fetch: app.fetch,
autoCleanupIncoming: false,
})
```
### `websocket`
provide a websocket server to enable websocket support.
```ts
import { serve, upgradeWebSocket } from '@hono/node-server'
import { WebSocketServer } from 'ws'
// ...
const wss = new WebSocketServer({ noServer: true })
serve({
fetch: app.fetch,
websocket: { server: wss },
})
```
## Middleware
Most built-in middleware also works with Node.js.
Read [the documentation](https://hono.dev/middleware/builtin/basic-auth) and use the Middleware of your liking.
```ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { prettyJSON } from 'hono/pretty-json'
const app = new Hono()
app.get('*', prettyJSON())
app.get('/', (c) => c.json({ 'Hono meets': 'Node.js' }))
serve(app)
```
## Serve Static Middleware
Use Serve Static Middleware that has been created for Node.js.
```ts
import { serveStatic } from '@hono/node-server/serve-static'
//...
app.use('/static/*', serveStatic({ root: './' }))
```
If using a relative path, `root` will be relative to the current working directory from which the app was started.
This can cause confusion when running your application locally.
Imagine your project structure is:
```
my-hono-project/
src/
index.ts
static/
index.html
```
Typically, you would run your app from the project's root directory (`my-hono-project`),
so you would need the following code to serve the `static` folder:
```ts
app.use('/static/*', serveStatic({ root: './static' }))
```
Notice that `root` here is not relative to `src/index.ts`, rather to `my-hono-project`.
### Options
#### `rewriteRequestPath`
If you want to serve files in `./.foojs` with the request path `/__foo/*`, you can write like the following.
```ts
app.use(
'/__foo/*',
serveStatic({
root: './.foojs/',
rewriteRequestPath: (path: string) => path.replace(/^\/__foo/, ''),
})
)
```
#### `onFound`
You can specify handling when the requested file is found with `onFound`.
```ts
app.use(
'/static/*',
serveStatic({
// ...
onFound: (_path, c) => {
c.header('Cache-Control', `public, immutable, max-age=31536000`)
},
})
)
```
#### `onNotFound`
The `onNotFound` is useful for debugging. You can write a handle for when a file is not found.
```ts
app.use(
'/static/*',
serveStatic({
root: './non-existent-dir',
onNotFound: (path, c) => {
console.log(`${path} is not found, request to ${c.req.path}`)
},
})
)
```
#### `precompressed`
The `precompressed` option checks if files with extensions like `.br` or `.gz` are available and serves them based on the `Accept-Encoding` header. It prioritizes Brotli, then Zstd, and Gzip. If none are available, it serves the original file.
```ts
app.use(
'/static/*',
serveStatic({
precompressed: true,
})
)
```
## ConnInfo Helper
You can use the [ConnInfo Helper](https://hono.dev/docs/helpers/conninfo) by importing `getConnInfo` from `@hono/node-server/conninfo`.
```ts
import { getConnInfo } from '@hono/node-server/conninfo'
app.get('/', (c) => {
const info = getConnInfo(c) // info is `ConnInfo`
return c.text(`Your remote address is ${info.remote.address}`)
})
```
## Accessing Node.js API
You can access the Node.js API from `c.env` in Node.js. For example, if you want to specify a type, you can write the following.
```ts
import { serve } from '@hono/node-server'
import type { HttpBindings } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono<{ Bindings: HttpBindings }>()
app.get('/', (c) => {
return c.json({
remoteAddress: c.env.incoming.socket.remoteAddress,
})
})
serve(app)
```
The APIs that you can get from `c.env` are as follows.
```ts
type HttpBindings = {
incoming: IncomingMessage
outgoing: ServerResponse
}
type Http2Bindings = {
incoming: Http2ServerRequest
outgoing: Http2ServerResponse
}
```
## Direct response from Node.js API
You can directly respond to the client from the Node.js API.
In that case, the response from Hono should be ignored, so return `RESPONSE_ALREADY_SENT`.
> [!NOTE]
> This feature can be used when migrating existing Node.js applications to Hono, but we recommend using Hono's API for new applications.
```ts
import { serve } from '@hono/node-server'
import type { HttpBindings } from '@hono/node-server'
import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response'
import { Hono } from 'hono'
const app = new Hono<{ Bindings: HttpBindings }>()
app.get('/', (c) => {
const { outgoing } = c.env
outgoing.writeHead(200, { 'Content-Type': 'text/plain' })
outgoing.end('Hello World\n')
return RESPONSE_ALREADY_SENT
})
serve(app)
```
## Listen to a UNIX domain socket
You can configure the HTTP server to listen to a UNIX domain socket instead of a TCP port.
```ts
import { createAdaptorServer } from '@hono/node-server'
// ...
const socketPath = '/tmp/example.sock'
const server = createAdaptorServer(app)
server.listen(socketPath, () => {
console.log(`Listening on ${socketPath}`)
})
```
## Related projects
- Hono - <https://hono.dev>
- Hono GitHub repository - <https://github.com/honojs/hono>
## Authors
- Yusuke Wada <https://github.com/yusukebe>
- Taku Amano <https://github.com/usualoma>
## License
MIT
+22
View File
@@ -0,0 +1,22 @@
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
//#region src/conninfo.ts
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
const getConnInfo = (c) => {
const bindings = c.env.server ? c.env.server : c.env;
const address = bindings.incoming.socket.remoteAddress;
const port = bindings.incoming.socket.remotePort;
const family = bindings.incoming.socket.remoteFamily;
return { remote: {
address,
port,
addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0
} };
};
//#endregion
exports.getConnInfo = getConnInfo;
+11
View File
@@ -0,0 +1,11 @@
import { GetConnInfo } from "hono/conninfo";
//#region src/conninfo.d.ts
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
declare const getConnInfo: GetConnInfo;
//#endregion
export { getConnInfo };
+11
View File
@@ -0,0 +1,11 @@
import { GetConnInfo } from "hono/conninfo";
//#region src/conninfo.d.ts
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
declare const getConnInfo: GetConnInfo;
//#endregion
export { getConnInfo };
+20
View File
@@ -0,0 +1,20 @@
//#region src/conninfo.ts
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
const getConnInfo = (c) => {
const bindings = c.env.server ? c.env.server : c.env;
const address = bindings.incoming.socket.remoteAddress;
const port = bindings.incoming.socket.remotePort;
const family = bindings.incoming.socket.remoteFamily;
return { remote: {
address,
port,
addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0
} };
};
//#endregion
export { getConnInfo };
+5
View File
@@ -0,0 +1,5 @@
//#region src/utils/response/constants.ts
const X_ALREADY_SENT = "x-hono-already-sent";
//#endregion
export { X_ALREADY_SENT as t };
+11
View File
@@ -0,0 +1,11 @@
//#region src/utils/response/constants.ts
const X_ALREADY_SENT = "x-hono-already-sent";
//#endregion
Object.defineProperty(exports, 'X_ALREADY_SENT', {
enumerable: true,
get: function () {
return X_ALREADY_SENT;
}
});
+1006
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
import { AddressInfo } from "node:net";
import { WebSocket, WebSocketServer } from "ws";
import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http";
import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2";
import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https";
import { UpgradeWebSocket } from "hono/ws";
//#region src/types.d.ts
type HttpBindings = {
incoming: IncomingMessage;
outgoing: ServerResponse;
};
type Http2Bindings = {
incoming: Http2ServerRequest;
outgoing: Http2ServerResponse;
};
type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise<unknown> | unknown;
type ServerType = Server | Http2Server | Http2SecureServer;
type createHttpOptions = {
serverOptions?: ServerOptions;
createServer?: typeof createServer;
};
type createHttpsOptions = {
serverOptions?: ServerOptions$2;
createServer?: typeof createServer$2;
};
type createHttp2Options = {
serverOptions?: ServerOptions$1;
createServer?: typeof createServer$1;
};
type createSecureHttp2Options = {
serverOptions?: SecureServerOptions;
createServer?: typeof createSecureServer;
};
type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options;
type Options = {
fetch: FetchCallback;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
port?: number;
hostname?: string;
websocket?: {
server: WebSocketServer;
};
} & ServerOptions$3;
type CustomErrorHandler = (err: unknown) => void | Response | Promise<void | Response>;
//#endregion
//#region src/server.d.ts
declare const createAdaptorServer: (options: Options) => ServerType;
declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType;
//#endregion
//#region src/websocket.d.ts
type UpgradeWebSocketOptions = {
onError: (err: unknown) => void;
};
declare const upgradeWebSocket: UpgradeWebSocket<WebSocket, UpgradeWebSocketOptions>;
//#endregion
//#region src/listener.d.ts
declare const getRequestListener: (fetchCallback: FetchCallback, options?: {
hostname?: string;
errorHandler?: CustomErrorHandler;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise<void>;
//#endregion
//#region src/error.d.ts
declare class RequestError extends Error {
constructor(message: string, options?: {
cause?: unknown;
});
}
//#endregion
export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, createAdaptorServer, getRequestListener, serve, upgradeWebSocket };
+73
View File
@@ -0,0 +1,73 @@
import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http";
import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2";
import { UpgradeWebSocket } from "hono/ws";
import { AddressInfo } from "node:net";
import { WebSocket, WebSocketServer } from "ws";
import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https";
//#region src/types.d.ts
type HttpBindings = {
incoming: IncomingMessage;
outgoing: ServerResponse;
};
type Http2Bindings = {
incoming: Http2ServerRequest;
outgoing: Http2ServerResponse;
};
type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise<unknown> | unknown;
type ServerType = Server | Http2Server | Http2SecureServer;
type createHttpOptions = {
serverOptions?: ServerOptions;
createServer?: typeof createServer;
};
type createHttpsOptions = {
serverOptions?: ServerOptions$2;
createServer?: typeof createServer$2;
};
type createHttp2Options = {
serverOptions?: ServerOptions$1;
createServer?: typeof createServer$1;
};
type createSecureHttp2Options = {
serverOptions?: SecureServerOptions;
createServer?: typeof createSecureServer;
};
type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options;
type Options = {
fetch: FetchCallback;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
port?: number;
hostname?: string;
websocket?: {
server: WebSocketServer;
};
} & ServerOptions$3;
type CustomErrorHandler = (err: unknown) => void | Response | Promise<void | Response>;
//#endregion
//#region src/server.d.ts
declare const createAdaptorServer: (options: Options) => ServerType;
declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType;
//#endregion
//#region src/websocket.d.ts
type UpgradeWebSocketOptions = {
onError: (err: unknown) => void;
};
declare const upgradeWebSocket: UpgradeWebSocket<WebSocket, UpgradeWebSocketOptions>;
//#endregion
//#region src/listener.d.ts
declare const getRequestListener: (fetchCallback: FetchCallback, options?: {
hostname?: string;
errorHandler?: CustomErrorHandler;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise<void>;
//#endregion
//#region src/error.d.ts
declare class RequestError extends Error {
constructor(message: string, options?: {
cause?: unknown;
});
}
//#endregion
export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, createAdaptorServer, getRequestListener, serve, upgradeWebSocket };
+1001
View File
File diff suppressed because it is too large Load Diff
+135
View File
@@ -0,0 +1,135 @@
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
let node_stream = require("node:stream");
let hono_utils_mime = require("hono/utils/mime");
let node_fs = require("node:fs");
let node_path = require("node:path");
let node_process = require("node:process");
//#region src/serve-static.ts
const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
const ENCODINGS = {
br: ".br",
zstd: ".zst",
gzip: ".gz"
};
const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS);
const pr54206Applied = () => {
const [major, minor] = node_process.versions.node.split(".").map((component) => parseInt(component));
return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18;
};
const useReadableToWeb = pr54206Applied();
const createStreamBody = (stream) => {
if (useReadableToWeb) return node_stream.Readable.toWeb(stream);
return new ReadableStream({
start(controller) {
stream.on("data", (chunk) => {
controller.enqueue(chunk);
});
stream.on("error", (err) => {
controller.error(err);
});
stream.on("end", () => {
controller.close();
});
},
cancel() {
stream.destroy();
}
});
};
const getStats = (path) => {
let stats;
try {
stats = (0, node_fs.statSync)(path);
} catch {}
return stats;
};
const tryDecode = (str, decoder) => {
try {
return decoder(str);
} catch {
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
try {
return decoder(match);
} catch {
return match;
}
});
}
};
const tryDecodeURI = (str) => tryDecode(str, decodeURI);
const serveStatic = (options = { root: "" }) => {
const root = options.root || "";
const optionPath = options.path;
if (root !== "" && !(0, node_fs.existsSync)(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`);
return async (c, next) => {
if (c.finalized) return next();
let filename;
if (optionPath) filename = optionPath;
else try {
filename = tryDecodeURI(c.req.path);
if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}/.test(filename)) throw new Error();
} catch {
await options.onNotFound?.(c.req.path, c);
return next();
}
let path = (0, node_path.join)(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename);
let stats = getStats(path);
if (stats && stats.isDirectory()) {
const indexFile = options.index ?? "index.html";
path = (0, node_path.join)(path, indexFile);
stats = getStats(path);
}
if (!stats) {
await options.onNotFound?.(path, c);
return next();
}
const mimeType = (0, hono_utils_mime.getMimeType)(path);
c.header("Content-Type", mimeType || "application/octet-stream");
if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim()));
for (const encoding of ENCODINGS_ORDERED_KEYS) {
if (!acceptEncodingSet.has(encoding)) continue;
const precompressedStats = getStats(path + ENCODINGS[encoding]);
if (precompressedStats) {
c.header("Content-Encoding", encoding);
c.header("Vary", "Accept-Encoding", { append: true });
stats = precompressedStats;
path = path + ENCODINGS[encoding];
break;
}
}
}
let result;
const size = stats.size;
const range = c.req.header("range") || "";
if (c.req.method == "HEAD" || c.req.method == "OPTIONS") {
c.header("Content-Length", size.toString());
c.status(200);
result = c.body(null);
} else if (!range) {
c.header("Content-Length", size.toString());
result = c.body(createStreamBody((0, node_fs.createReadStream)(path)), 200);
} else {
c.header("Accept-Ranges", "bytes");
c.header("Date", stats.birthtime.toUTCString());
const parts = range.replace(/bytes=/, "").split("-", 2);
const start = parseInt(parts[0], 10) || 0;
let end = parseInt(parts[1], 10) || size - 1;
if (size < end - start + 1) end = size - 1;
const chunksize = end - start + 1;
const stream = (0, node_fs.createReadStream)(path, {
start,
end
});
c.header("Content-Length", chunksize.toString());
c.header("Content-Range", `bytes ${start}-${end}/${stats.size}`);
result = c.body(createStreamBody(stream), 206);
}
await options.onFound?.(path, c);
return result;
};
};
//#endregion
exports.serveStatic = serveStatic;
+18
View File
@@ -0,0 +1,18 @@
import { Context, Env, MiddlewareHandler } from "hono";
//#region src/serve-static.d.ts
type ServeStaticOptions<E extends Env = Env> = {
/**
* Root path, relative to current working directory from which the app was started. Absolute paths are not supported.
*/
root?: string;
path?: string;
index?: string;
precompressed?: boolean;
rewriteRequestPath?: (path: string, c: Context<E>) => string;
onFound?: (path: string, c: Context<E>) => void | Promise<void>;
onNotFound?: (path: string, c: Context<E>) => void | Promise<void>;
};
declare const serveStatic: <E extends Env = any>(options?: ServeStaticOptions<E>) => MiddlewareHandler<E>;
//#endregion
export { ServeStaticOptions, serveStatic };
+18
View File
@@ -0,0 +1,18 @@
import { Context, Env, MiddlewareHandler } from "hono";
//#region src/serve-static.d.ts
type ServeStaticOptions<E extends Env = Env> = {
/**
* Root path, relative to current working directory from which the app was started. Absolute paths are not supported.
*/
root?: string;
path?: string;
index?: string;
precompressed?: boolean;
rewriteRequestPath?: (path: string, c: Context<E>) => string;
onFound?: (path: string, c: Context<E>) => void | Promise<void>;
onNotFound?: (path: string, c: Context<E>) => void | Promise<void>;
};
declare const serveStatic: <E extends Env = any>(options?: ServeStaticOptions<E>) => MiddlewareHandler<E>;
//#endregion
export { ServeStaticOptions, serveStatic };
+134
View File
@@ -0,0 +1,134 @@
import { Readable } from "node:stream";
import { getMimeType } from "hono/utils/mime";
import { createReadStream, existsSync, statSync } from "node:fs";
import { join } from "node:path";
import { versions } from "node:process";
//#region src/serve-static.ts
const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
const ENCODINGS = {
br: ".br",
zstd: ".zst",
gzip: ".gz"
};
const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS);
const pr54206Applied = () => {
const [major, minor] = versions.node.split(".").map((component) => parseInt(component));
return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18;
};
const useReadableToWeb = pr54206Applied();
const createStreamBody = (stream) => {
if (useReadableToWeb) return Readable.toWeb(stream);
return new ReadableStream({
start(controller) {
stream.on("data", (chunk) => {
controller.enqueue(chunk);
});
stream.on("error", (err) => {
controller.error(err);
});
stream.on("end", () => {
controller.close();
});
},
cancel() {
stream.destroy();
}
});
};
const getStats = (path) => {
let stats;
try {
stats = statSync(path);
} catch {}
return stats;
};
const tryDecode = (str, decoder) => {
try {
return decoder(str);
} catch {
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
try {
return decoder(match);
} catch {
return match;
}
});
}
};
const tryDecodeURI = (str) => tryDecode(str, decodeURI);
const serveStatic = (options = { root: "" }) => {
const root = options.root || "";
const optionPath = options.path;
if (root !== "" && !existsSync(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`);
return async (c, next) => {
if (c.finalized) return next();
let filename;
if (optionPath) filename = optionPath;
else try {
filename = tryDecodeURI(c.req.path);
if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}/.test(filename)) throw new Error();
} catch {
await options.onNotFound?.(c.req.path, c);
return next();
}
let path = join(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename);
let stats = getStats(path);
if (stats && stats.isDirectory()) {
const indexFile = options.index ?? "index.html";
path = join(path, indexFile);
stats = getStats(path);
}
if (!stats) {
await options.onNotFound?.(path, c);
return next();
}
const mimeType = getMimeType(path);
c.header("Content-Type", mimeType || "application/octet-stream");
if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim()));
for (const encoding of ENCODINGS_ORDERED_KEYS) {
if (!acceptEncodingSet.has(encoding)) continue;
const precompressedStats = getStats(path + ENCODINGS[encoding]);
if (precompressedStats) {
c.header("Content-Encoding", encoding);
c.header("Vary", "Accept-Encoding", { append: true });
stats = precompressedStats;
path = path + ENCODINGS[encoding];
break;
}
}
}
let result;
const size = stats.size;
const range = c.req.header("range") || "";
if (c.req.method == "HEAD" || c.req.method == "OPTIONS") {
c.header("Content-Length", size.toString());
c.status(200);
result = c.body(null);
} else if (!range) {
c.header("Content-Length", size.toString());
result = c.body(createStreamBody(createReadStream(path)), 200);
} else {
c.header("Accept-Ranges", "bytes");
c.header("Date", stats.birthtime.toUTCString());
const parts = range.replace(/bytes=/, "").split("-", 2);
const start = parseInt(parts[0], 10) || 0;
let end = parseInt(parts[1], 10) || size - 1;
if (size < end - start + 1) end = size - 1;
const chunksize = end - start + 1;
const stream = createReadStream(path, {
start,
end
});
c.header("Content-Length", chunksize.toString());
c.header("Content-Range", `bytes ${start}-${end}/${stats.size}`);
result = c.body(createStreamBody(stream), 206);
}
await options.onFound?.(path, c);
return result;
};
};
//#endregion
export { serveStatic };
+8
View File
@@ -0,0 +1,8 @@
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const require_constants = require('../constants-BXAKTxRC.cjs');
//#region src/utils/response.ts
const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [require_constants.X_ALREADY_SENT]: "true" } });
//#endregion
exports.RESPONSE_ALREADY_SENT = RESPONSE_ALREADY_SENT;
+4
View File
@@ -0,0 +1,4 @@
//#region src/utils/response.d.ts
declare const RESPONSE_ALREADY_SENT: Response;
//#endregion
export { RESPONSE_ALREADY_SENT };
+4
View File
@@ -0,0 +1,4 @@
//#region src/utils/response.d.ts
declare const RESPONSE_ALREADY_SENT: Response;
//#endregion
export { RESPONSE_ALREADY_SENT };
+7
View File
@@ -0,0 +1,7 @@
import { t as X_ALREADY_SENT } from "../constants-BLSFu_RU.mjs";
//#region src/utils/response.ts
const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [X_ALREADY_SENT]: "true" } });
//#endregion
export { RESPONSE_ALREADY_SENT };
Generated Vendored Executable
+116
View File
@@ -0,0 +1,116 @@
{
"name": "@hono/node-server",
"version": "2.0.0",
"description": "Node.js Adapter for Hono",
"main": "dist/index.mjs",
"type": "module",
"types": "dist/index.d.mts",
"files": [
"dist"
],
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./serve-static": {
"import": {
"types": "./dist/serve-static.d.mts",
"default": "./dist/serve-static.mjs"
},
"require": {
"types": "./dist/serve-static.d.cts",
"default": "./dist/serve-static.cjs"
}
},
"./utils/*": {
"import": {
"types": "./dist/utils/*.d.mts",
"default": "./dist/utils/*.mjs"
},
"require": {
"types": "./dist/utils/*.d.cts",
"default": "./dist/utils/*.cjs"
}
},
"./conninfo": {
"import": {
"types": "./dist/conninfo.d.mts",
"default": "./dist/conninfo.mjs"
},
"require": {
"types": "./dist/conninfo.d.cts",
"default": "./dist/conninfo.cjs"
}
}
},
"typesVersions": {
"*": {
".": [
"./dist/index.d.mts"
],
"serve-static": [
"./dist/serve-static.d.mts"
],
"utils/*": [
"./dist/utils/*.d.mts"
],
"conninfo": [
"./dist/conninfo.d.mts"
]
}
},
"scripts": {
"test": "vitest",
"build": "tsdown --external hono",
"watch": "tsdown --watch",
"postbuild": "publint",
"prerelease": "bun run build && bun run test --run",
"release": "np",
"lint": "eslint src test",
"lint:fix": "eslint src test --fix",
"format": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"",
"format:fix": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\""
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/honojs/node-server.git"
},
"homepage": "https://github.com/honojs/node-server",
"author": "Yusuke Wada <yusuke@kamawada.com> (https://github.com/yusukebe)",
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"engines": {
"node": ">=20"
},
"devDependencies": {
"@hono/eslint-config": "^1.0.1",
"@types/node": "^20.10.0",
"@types/supertest": "^2.0.12",
"@types/ws": "^8.18.1",
"@whatwg-node/fetch": "^0.9.14",
"eslint": "^9.10.0",
"hono": "^4.12.8",
"np": "^7.7.0",
"prettier": "^3.2.4",
"publint": "^0.3.18",
"supertest": "^6.3.3",
"tsdown": "^0.20.3",
"typescript": "^5.3.2",
"vitest": "^4.0.18",
"ws": "^8.19.0"
},
"peerDependencies": {
"hono": "^4"
},
"packageManager": "bun@1.2.20"
}