@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 +41 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +218 -0
- package/dist/index.js.map +11 -0
- package/dist/static.module.d.ts +40 -0
- package/dist/static.service.d.ts +37 -0
- package/package.json +26 -0
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).
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|