@nexusts/static 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @nexusts/static
2
+
3
+ > **NexusTS** — Bun-native fullstack framework
4
+
5
+ ## Description
6
+
7
+ Static file serving (ETag / Range / MIME).
8
+
9
+ ETag, Range (HTTP 206), MIME detection, path-traversal protection. Serve assets from a directory.
10
+
11
+ ## Install
12
+
13
+ This module is part of the NexusTS monorepo. Each module is published as its own npm package under the `@nexusts/` scope.
14
+
15
+ Most apps start with just the core:
16
+
17
+ ```bash
18
+ bun add @nexusts/core
19
+ ```
20
+
21
+ Then add this module only if you need it:
22
+
23
+ ```bash
24
+ bun add @nexusts/static
25
+ ```
26
+
27
+ ## Peer dependencies
28
+
29
+ **None.** No external dependencies.
30
+
31
+ ## Usage
32
+
33
+ ```typescript
34
+ import { /* public API */ } from "@nexusts/static";
35
+ ```
36
+
37
+ See the [user guide](../../docs/user-guide/static.md) and the [example app](../../examples/) for a working demo.
38
+
39
+ ## License
40
+
41
+ MIT — see the root [LICENSE](../../LICENSE).
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Public API for `nexusjs/static`.
3
+ *
4
+ * Quick start:
5
+ *
6
+ * // src/app/app.module.ts
7
+ * import { Module } from 'nexusjs';
8
+ * import { StaticModule } from 'nexusjs/static';
9
+ *
10
+ * @Module({
11
+ * imports: [
12
+ * StaticModule.forRoot({
13
+ * root: './public',
14
+ * prefix: '/public',
15
+ * }),
16
+ * ],
17
+ * })
18
+ * export class AppModule {}
19
+ *
20
+ * // any service that needs to mount the middleware on a sub-app
21
+ * import { StaticService } from 'nexusjs/static';
22
+ *
23
+ * @Injectable()
24
+ * class CustomServer {
25
+ * constructor(@Inject(StaticService.TOKEN) private static: StaticService) {}
26
+ * mount(app: Hono) {
27
+ * app.use('/public/*', this.static.middleware());
28
+ * }
29
+ * }
30
+ *
31
+ * Features:
32
+ * - Path-traversal protection (no `..`, no absolute paths)
33
+ * - ETag-based conditional GET (304 Not Modified)
34
+ * - Range requests (HTTP 206) for video / large files
35
+ * - Sensible `Cache-Control` defaults
36
+ * - `index.html` fallback for directory requests
37
+ * - MIME-type inference for common formats
38
+ */
39
+ export { StaticService, type ServeStaticOptions } from "./static.service.js";
40
+ export { StaticModule } from "./static.module.js";
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ // @bun
2
+ var __legacyDecorateClassTS = function(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
5
+ r = Reflect.decorate(decorators, target, key, desc);
6
+ else
7
+ for (var i = decorators.length - 1;i >= 0; i--)
8
+ if (d = decorators[i])
9
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
10
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
11
+ };
12
+
13
+ // packages/static/src/static.service.ts
14
+ import { createReadStream } from "fs";
15
+ import { stat, open } from "fs/promises";
16
+ import { resolve as pathResolve, normalize, join, sep } from "path";
17
+
18
+ class StaticService {
19
+ static TOKEN = Symbol.for("nexus:StaticService");
20
+ #root;
21
+ #prefix;
22
+ #index;
23
+ #cacheControl;
24
+ #etag;
25
+ #range;
26
+ #maxFileSize;
27
+ constructor(options = {}) {
28
+ this.#root = pathResolve(options.root ?? "./public");
29
+ const prefix = options.prefix ?? "/";
30
+ this.#prefix = prefix === "/" ? "/" : prefix.replace(/\/$/, "");
31
+ this.#index = options.index === false ? false : options.index ?? "index.html";
32
+ this.#cacheControl = options.cacheControl ?? "public, max-age=3600";
33
+ this.#etag = options.etag ?? true;
34
+ this.#range = options.range ?? true;
35
+ this.#maxFileSize = options.maxFileSize ?? 100 * 1024 * 1024;
36
+ }
37
+ middleware() {
38
+ return async (c, next) => {
39
+ const url = new URL(c.req.url);
40
+ const pathname = decodeURIComponent(url.pathname);
41
+ if (!pathname.startsWith(this.#prefix)) {
42
+ return next();
43
+ }
44
+ const rel = pathname.slice(this.#prefix.length).replace(/^\//, "");
45
+ const safe = this.#safeResolve(rel);
46
+ if (!safe)
47
+ return next();
48
+ let statResult;
49
+ try {
50
+ statResult = await stat(safe);
51
+ } catch {
52
+ return next();
53
+ }
54
+ let filePath = safe;
55
+ if (statResult.isDirectory()) {
56
+ if (this.#index === false)
57
+ return next();
58
+ filePath = join(safe, this.#index);
59
+ try {
60
+ const idx = await stat(filePath);
61
+ if (!idx.isFile())
62
+ return next();
63
+ } catch {
64
+ return next();
65
+ }
66
+ }
67
+ let fileStat;
68
+ try {
69
+ fileStat = await stat(filePath);
70
+ } catch {
71
+ return next();
72
+ }
73
+ const size = fileStat.size;
74
+ if (size > this.#maxFileSize)
75
+ return next();
76
+ const etag = this.#etag ? `"${this.#computeEtag(filePath, size, fileStat.mtimeMs)}"` : null;
77
+ if (etag) {
78
+ const inm = c.req.header("if-none-match");
79
+ if (inm && inm === etag) {
80
+ return new Response(null, { status: 304 });
81
+ }
82
+ c.header("ETag", etag);
83
+ }
84
+ const range = c.req.header("range");
85
+ if (this.#range && range) {
86
+ const m = /^bytes=(\d*)-(\d*)$/.exec(range);
87
+ if (m) {
88
+ const start = m[1] ? Number(m[1]) : 0;
89
+ const end = m[2] ? Number(m[2]) : size - 1;
90
+ if (start >= size || end >= size || start > end) {
91
+ return new Response("Range Not Satisfiable", {
92
+ status: 416,
93
+ headers: { "Content-Range": `bytes */${size}` }
94
+ });
95
+ }
96
+ const slice = await this.#readSlice(filePath, start, end);
97
+ return new Response(slice, {
98
+ status: 206,
99
+ headers: {
100
+ "Content-Type": this.#mime(filePath),
101
+ "Content-Length": String(end - start + 1),
102
+ "Content-Range": `bytes ${start}-${end}/${size}`,
103
+ "Cache-Control": this.#cacheControl,
104
+ "Accept-Ranges": "bytes"
105
+ }
106
+ });
107
+ }
108
+ }
109
+ const body = createReadStream(filePath);
110
+ return new Response(body, {
111
+ status: 200,
112
+ headers: {
113
+ "Content-Type": this.#mime(filePath),
114
+ "Content-Length": String(size),
115
+ "Cache-Control": this.#cacheControl,
116
+ "Accept-Ranges": "bytes",
117
+ ...etag ? { ETag: etag } : {}
118
+ }
119
+ });
120
+ };
121
+ }
122
+ #safeResolve(rel) {
123
+ if (rel.includes(".."))
124
+ return null;
125
+ if (rel.startsWith("/") || /^[a-zA-Z]:/.test(rel))
126
+ return null;
127
+ const joined = join(this.#root, rel);
128
+ const norm = normalize(joined);
129
+ if (!norm.startsWith(this.#root + sep) && norm !== this.#root)
130
+ return null;
131
+ return norm;
132
+ }
133
+ #computeEtag(_path, size, mtimeMs) {
134
+ const bucket = Math.floor(mtimeMs / 1000);
135
+ const hash = `${size}-${bucket}`;
136
+ return Buffer.from(hash).toString("base64url");
137
+ }
138
+ async#readSlice(path, start, end) {
139
+ const fh = await open(path, "r");
140
+ try {
141
+ const length = end - start + 1;
142
+ const buf = Buffer.alloc(length);
143
+ await fh.read(buf, 0, length, start);
144
+ return buf;
145
+ } finally {
146
+ await fh.close();
147
+ }
148
+ }
149
+ #mime(path) {
150
+ const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
151
+ const map = {
152
+ html: "text/html; charset=utf-8",
153
+ htm: "text/html; charset=utf-8",
154
+ css: "text/css; charset=utf-8",
155
+ js: "application/javascript; charset=utf-8",
156
+ mjs: "application/javascript; charset=utf-8",
157
+ json: "application/json; charset=utf-8",
158
+ svg: "image/svg+xml",
159
+ png: "image/png",
160
+ jpeg: "image/jpeg",
161
+ jpg: "image/jpeg",
162
+ gif: "image/gif",
163
+ webp: "image/webp",
164
+ ico: "image/x-icon",
165
+ pdf: "application/pdf",
166
+ txt: "text/plain; charset=utf-8",
167
+ woff: "font/woff",
168
+ woff2: "font/woff2",
169
+ mp4: "video/mp4",
170
+ webm: "video/webm",
171
+ mp3: "audio/mpeg"
172
+ };
173
+ return map[ext] ?? "application/octet-stream";
174
+ }
175
+ }
176
+ // packages/static/src/static.module.ts
177
+ import"reflect-metadata";
178
+ import { Module } from "@nexusts/core";
179
+ class StaticModule {
180
+ static forRoot(options = {}) {
181
+ class ConfiguredStaticModule {
182
+ }
183
+ ConfiguredStaticModule = __legacyDecorateClassTS([
184
+ Module({
185
+ providers: [
186
+ StaticService,
187
+ { provide: StaticService.TOKEN, useExisting: StaticService },
188
+ { provide: "STATIC_OPTIONS", useValue: options }
189
+ ],
190
+ exports: [StaticService, StaticService.TOKEN]
191
+ })
192
+ ], ConfiguredStaticModule);
193
+ Object.defineProperty(ConfiguredStaticModule, "name", {
194
+ value: "ConfiguredStaticModule"
195
+ });
196
+ return ConfiguredStaticModule;
197
+ }
198
+ static mount(options = {}) {
199
+ const svc = new StaticService(options);
200
+ return svc.middleware();
201
+ }
202
+ }
203
+ StaticModule = __legacyDecorateClassTS([
204
+ Module({
205
+ providers: [
206
+ StaticService,
207
+ { provide: StaticService.TOKEN, useExisting: StaticService }
208
+ ],
209
+ exports: [StaticService, StaticService.TOKEN]
210
+ })
211
+ ], StaticModule);
212
+ export {
213
+ StaticService,
214
+ StaticModule
215
+ };
216
+
217
+ //# debugId=D088EA1AD2A5EE3164756E2164756E21
218
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/static.service.ts", "../src/static.module.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Static file serving — middleware + helpers.\n *\n * Mirrors `@adonisjs/static` + `serve-static`. Built on the platform's\n * native filesystem (Bun.file() on Bun, node:fs elsewhere).\n */\n\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { createReadStream } from \"node:fs\";\nimport { stat, open } from \"node:fs/promises\";\nimport { resolve as pathResolve, normalize, join, sep } from \"node:path\";\n\n/** Configuration for `serveStatic`. */\nexport interface ServeStaticOptions {\n\t/** Filesystem root to serve files from. Default: `./public`. */\n\troot?: string;\n\t/** URL prefix. Default: `/`. */\n\tprefix?: string;\n\t/** Default file when a directory is requested. Default: `index.html`. */\n\tindex?: string | false;\n\t/** `Cache-Control` header. Default: `'public, max-age=3600'`. */\n\tcacheControl?: string;\n\t/** Enable ETag generation. Default: `true`. */\n\tetag?: boolean;\n\t/** Enable range requests (for video / large files). Default: `true`. */\n\trange?: boolean;\n\t/** Max file size in bytes; larger files return 404. Default: 100 MB. */\n\tmaxFileSize?: number;\n}\n\n/** StaticService — emits the `serveStatic` middleware. */\nexport class StaticService {\n\t/** DI token — use with `@Inject(StaticService.TOKEN)`. */\n\tstatic readonly TOKEN = Symbol.for(\"nexus:StaticService\");\n\n\t#root: string;\n\t#prefix: string;\n\t#index: string | false;\n\t#cacheControl: string;\n\t#etag: boolean;\n\t#range: boolean;\n\t#maxFileSize: number;\n\n\tconstructor(options: ServeStaticOptions = {}) {\n\t\tthis.#root = pathResolve(options.root ?? \"./public\");\n\t\t// Normalize prefix to always start with `/` and end without `/` (except root).\n\t\tconst prefix = options.prefix ?? \"/\";\n\t\tthis.#prefix = prefix === \"/\" ? \"/\" : prefix.replace(/\\/$/, \"\");\n\t\tthis.#index =\n\t\t\toptions.index === false ? false : (options.index ?? \"index.html\");\n\t\tthis.#cacheControl = options.cacheControl ?? \"public, max-age=3600\";\n\t\tthis.#etag = options.etag ?? true;\n\t\tthis.#range = options.range ?? true;\n\t\tthis.#maxFileSize = options.maxFileSize ?? 100 * 1024 * 1024;\n\t}\n\n\t/**\n\t * Build a Hono middleware that serves files from `root`.\n\t *\n\t * app.use('/public/*', staticService.middleware());\n\t */\n\tmiddleware(): MiddlewareHandler {\n\t\treturn async (c, next) => {\n\t\t\tconst url = new URL(c.req.url);\n\t\t\tconst pathname = decodeURIComponent(url.pathname);\n\t\t\tif (!pathname.startsWith(this.#prefix)) {\n\t\t\t\treturn next();\n\t\t\t}\n\t\t\t// pathname e.g. \"/static/test.html\", prefix e.g. \"/static\"\n\t\t\t// → slice gives \"/test.html\". Strip leading \"/\" so\n\t\t\t// #safeResolve doesn't reject it as an absolute path.\n\t\t\tconst rel = pathname.slice(this.#prefix.length).replace(/^\\//, \"\");\n\t\t\tconst safe = this.#safeResolve(rel);\n\t\t\tif (!safe) return next();\n\n\t\t\tlet statResult: Awaited<ReturnType<typeof stat>>;\n\t\t\ttry {\n\t\t\t\tstatResult = await stat(safe);\n\t\t\t} catch {\n\t\t\t\treturn next();\n\t\t\t}\n\n\t\t\tlet filePath = safe;\n\t\t\tif (statResult.isDirectory()) {\n\t\t\t\tif (this.#index === false) return next();\n\t\t\t\tfilePath = join(safe, this.#index);\n\t\t\t\ttry {\n\t\t\t\t\tconst idx = await stat(filePath);\n\t\t\t\t\tif (!idx.isFile()) return next();\n\t\t\t\t} catch {\n\t\t\t\t\treturn next();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Size guard.\n\t\t\tlet fileStat: Awaited<ReturnType<typeof stat>>;\n\t\t\ttry {\n\t\t\t\tfileStat = await stat(filePath);\n\t\t\t} catch {\n\t\t\t\treturn next();\n\t\t\t}\n\t\t\tconst size = fileStat.size;\n\t\t\tif (size > this.#maxFileSize) return next();\n\n\t\t\t// ETag.\n\t\t\tconst etag = this.#etag\n\t\t\t\t? `\"${this.#computeEtag(filePath, size, fileStat.mtimeMs)}\"`\n\t\t\t\t: null;\n\t\t\tif (etag) {\n\t\t\t\tconst inm = c.req.header(\"if-none-match\");\n\t\t\t\tif (inm && inm === etag) {\n\t\t\t\t\treturn new Response(null, { status: 304 });\n\t\t\t\t}\n\t\t\t\tc.header(\"ETag\", etag);\n\t\t\t}\n\n\t\t\t// Range requests.\n\t\t\tconst range = c.req.header(\"range\");\n\t\t\tif (this.#range && range) {\n\t\t\t\tconst m = /^bytes=(\\d*)-(\\d*)$/.exec(range);\n\t\t\t\tif (m) {\n\t\t\t\t\tconst start = m[1] ? Number(m[1]) : 0;\n\t\t\t\t\tconst end = m[2] ? Number(m[2]) : size - 1;\n\t\t\t\t\tif (start >= size || end >= size || start > end) {\n\t\t\t\t\t\treturn new Response(\"Range Not Satisfiable\", {\n\t\t\t\t\t\t\tstatus: 416,\n\t\t\t\t\t\t\theaders: { \"Content-Range\": `bytes */${size}` },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst slice = await this.#readSlice(filePath, start, end);\n\t\t\t\t\treturn new Response(slice as BodyInit, {\n\t\t\t\t\t\tstatus: 206,\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\"Content-Type\": this.#mime(filePath),\n\t\t\t\t\t\t\t\"Content-Length\": String(end - start + 1),\n\t\t\t\t\t\t\t\"Content-Range\": `bytes ${start}-${end}/${size}`,\n\t\t\t\t\t\t\t\"Cache-Control\": this.#cacheControl,\n\t\t\t\t\t\t\t\"Accept-Ranges\": \"bytes\",\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Full body.\n\t\t\tconst body = createReadStream(filePath);\n\t\t\treturn new Response(body as unknown as ReadableStream, {\n\t\t\t\tstatus: 200,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": this.#mime(filePath),\n\t\t\t\t\t\"Content-Length\": String(size),\n\t\t\t\t\t\"Cache-Control\": this.#cacheControl,\n\t\t\t\t\t\"Accept-Ranges\": \"bytes\",\n\t\t\t\t\t...(etag ? { ETag: etag } : {}),\n\t\t\t\t},\n\t\t\t});\n\t\t};\n\t}\n\n\t// ===========================================================================\n\t// Internal\n\t// ===========================================================================\n\n\t#safeResolve(rel: string): string | null {\n\t\t// Reject path traversal: no `..`, no absolute paths, no drive letters.\n\t\tif (rel.includes(\"..\")) return null;\n\t\tif (rel.startsWith(\"/\") || /^[a-zA-Z]:/.test(rel)) return null;\n\t\tconst joined = join(this.#root, rel);\n\t\tconst norm = normalize(joined);\n\t\t// Make sure the result is still under `root`.\n\t\tif (!norm.startsWith(this.#root + sep) && norm !== this.#root) return null;\n\t\treturn norm;\n\t}\n\n\t#computeEtag(_path: string, size: number, mtimeMs: number): string {\n\t\t// Simple hash: size + mtime bucket.\n\t\tconst bucket = Math.floor(mtimeMs / 1000);\n\t\tconst hash = `${size}-${bucket}`;\n\t\treturn Buffer.from(hash).toString(\"base64url\");\n\t}\n\n\tasync #readSlice(path: string, start: number, end: number): Promise<Buffer> {\n\t\tconst fh = await open(path, \"r\");\n\t\ttry {\n\t\t\tconst length = end - start + 1;\n\t\t\tconst buf = Buffer.alloc(length);\n\t\t\tawait fh.read(buf, 0, length, start);\n\t\t\treturn buf;\n\t\t} finally {\n\t\t\tawait fh.close();\n\t\t}\n\t}\n\n\t#mime(path: string): string {\n\t\tconst ext = path.slice(path.lastIndexOf(\".\") + 1).toLowerCase();\n\t\tconst map: Record<string, string> = {\n\t\t\thtml: \"text/html; charset=utf-8\",\n\t\t\thtm: \"text/html; charset=utf-8\",\n\t\t\tcss: \"text/css; charset=utf-8\",\n\t\t\tjs: \"application/javascript; charset=utf-8\",\n\t\t\tmjs: \"application/javascript; charset=utf-8\",\n\t\t\tjson: \"application/json; charset=utf-8\",\n\t\t\tsvg: \"image/svg+xml\",\n\t\t\tpng: \"image/png\",\n\t\t\tjpeg: \"image/jpeg\",\n\t\t\tjpg: \"image/jpeg\",\n\t\t\tgif: \"image/gif\",\n\t\t\twebp: \"image/webp\",\n\t\t\tico: \"image/x-icon\",\n\t\t\tpdf: \"application/pdf\",\n\t\t\ttxt: \"text/plain; charset=utf-8\",\n\t\t\twoff: \"font/woff\",\n\t\t\twoff2: \"font/woff2\",\n\t\t\tmp4: \"video/mp4\",\n\t\t\twebm: \"video/webm\",\n\t\t\tmp3: \"audio/mpeg\",\n\t\t};\n\t\treturn map[ext] ?? \"application/octet-stream\";\n\t}\n}\n",
6
+ "/**\n * `StaticModule` — drop-in module for static file serving.\n *\n * Usage:\n * @Module({\n * imports: [\n * StaticModule.forRoot({\n * root: './public',\n * prefix: '/public',\n * cacheControl: 'public, max-age=86400',\n * }),\n * ],\n * })\n * export class AppModule {}\n *\n * Then `GET /public/*` serves files from `./public/*` with proper\n * Content-Type, ETag, and Range support.\n */\n\nimport \"reflect-metadata\";\nimport { Module } from \"@nexusts/core\";\nimport { StaticService } from \"./static.service.js\";\nimport type { ServeStaticOptions } from \"./static.service.js\";\n\n@Module({\n\tproviders: [\n\t\tStaticService,\n\t\t{ provide: StaticService.TOKEN, useExisting: StaticService },\n\t],\n\texports: [StaticService, StaticService.TOKEN],\n})\nexport class StaticModule {\n\tstatic forRoot(options: ServeStaticOptions = {}) {\n\t\t@Module({\n\t\t\tproviders: [\n\t\t\t\tStaticService,\n\t\t\t\t{ provide: StaticService.TOKEN, useExisting: StaticService },\n\t\t\t\t{ provide: \"STATIC_OPTIONS\", useValue: options },\n\t\t\t],\n\t\t\texports: [StaticService, StaticService.TOKEN],\n\t\t})\n\t\tclass ConfiguredStaticModule {}\n\n\t\tObject.defineProperty(ConfiguredStaticModule, \"name\", {\n\t\t\tvalue: \"ConfiguredStaticModule\",\n\t\t});\n\n\t\treturn ConfiguredStaticModule;\n\t}\n\n\t/**\n\t * Convenience: create a middleware handler and mount it directly\n\t * on the Hono app. Useful in `main.ts` to serve static files\n\t * without going through DI.\n\t *\n\t * ```ts\n\t * import { StaticModule } from 'nexusjs/static';\n\t * const app = new Application(AppModule);\n\t * app.server.app.use('/static/*', StaticModule.mount({\n\t * root: './public',\n\t * prefix: '/static',\n\t * }));\n\t * ```\n\t */\n\tstatic mount(options: ServeStaticOptions = {}) {\n\t\tconst svc = new StaticService(options);\n\t\treturn svc.middleware();\n\t}\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "D088EA1AD2A5EE3164756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `StaticModule` — drop-in module for static file serving.
3
+ *
4
+ * Usage:
5
+ * @Module({
6
+ * imports: [
7
+ * StaticModule.forRoot({
8
+ * root: './public',
9
+ * prefix: '/public',
10
+ * cacheControl: 'public, max-age=86400',
11
+ * }),
12
+ * ],
13
+ * })
14
+ * export class AppModule {}
15
+ *
16
+ * Then `GET /public/*` serves files from `./public/*` with proper
17
+ * Content-Type, ETag, and Range support.
18
+ */
19
+ import "reflect-metadata";
20
+ import type { ServeStaticOptions } from "./static.service.js";
21
+ export declare class StaticModule {
22
+ static forRoot(options?: ServeStaticOptions): {
23
+ new (): {};
24
+ };
25
+ /**
26
+ * Convenience: create a middleware handler and mount it directly
27
+ * on the Hono app. Useful in `main.ts` to serve static files
28
+ * without going through DI.
29
+ *
30
+ * ```ts
31
+ * import { StaticModule } from 'nexusjs/static';
32
+ * const app = new Application(AppModule);
33
+ * app.server.app.use('/static/*', StaticModule.mount({
34
+ * root: './public',
35
+ * prefix: '/static',
36
+ * }));
37
+ * ```
38
+ */
39
+ static mount(options?: ServeStaticOptions): import("hono").MiddlewareHandler;
40
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Static file serving — middleware + helpers.
3
+ *
4
+ * Mirrors `@adonisjs/static` + `serve-static`. Built on the platform's
5
+ * native filesystem (Bun.file() on Bun, node:fs elsewhere).
6
+ */
7
+ import type { MiddlewareHandler } from "hono";
8
+ /** Configuration for `serveStatic`. */
9
+ export interface ServeStaticOptions {
10
+ /** Filesystem root to serve files from. Default: `./public`. */
11
+ root?: string;
12
+ /** URL prefix. Default: `/`. */
13
+ prefix?: string;
14
+ /** Default file when a directory is requested. Default: `index.html`. */
15
+ index?: string | false;
16
+ /** `Cache-Control` header. Default: `'public, max-age=3600'`. */
17
+ cacheControl?: string;
18
+ /** Enable ETag generation. Default: `true`. */
19
+ etag?: boolean;
20
+ /** Enable range requests (for video / large files). Default: `true`. */
21
+ range?: boolean;
22
+ /** Max file size in bytes; larger files return 404. Default: 100 MB. */
23
+ maxFileSize?: number;
24
+ }
25
+ /** StaticService — emits the `serveStatic` middleware. */
26
+ export declare class StaticService {
27
+ #private;
28
+ /** DI token — use with `@Inject(StaticService.TOKEN)`. */
29
+ static readonly TOKEN: unique symbol;
30
+ constructor(options?: ServeStaticOptions);
31
+ /**
32
+ * Build a Hono middleware that serves files from `root`.
33
+ *
34
+ * app.use('/public/*', staticService.middleware());
35
+ */
36
+ middleware(): MiddlewareHandler;
37
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@nexusts/static",
3
+ "version": "0.7.2",
4
+ "description": "Static file serving (ETag / Range / MIME)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist", "README.md"],
16
+ "scripts": {
17
+ "build": "bun run ../../build.ts"
18
+ },
19
+ "keywords": ["nexusts", "framework", "bun"],
20
+ "license": "MIT",
21
+
22
+
23
+ "dependencies": {
24
+ "@nexusts/core": "file:../core"
25
+ }
26
+ }