@ripple-ts/adapter-bun 0.2.212 → 0.2.214
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 +14 -0
- package/package.json +6 -3
- package/src/index.js +87 -7
- package/tests/serve.test.js +86 -19
- package/types/index.d.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @ripple-ts/adapter-bun
|
|
2
2
|
|
|
3
|
+
## 0.2.214
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies []:
|
|
8
|
+
- @ripple-ts/adapter@0.2.214
|
|
9
|
+
|
|
10
|
+
## 0.2.213
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies []:
|
|
15
|
+
- @ripple-ts/adapter@0.2.213
|
|
16
|
+
|
|
3
17
|
## 0.2.212
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Bun adapter for Ripple metaframework (Web Request/Response bridge)",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.214",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/index.js",
|
|
9
9
|
"main": "src/index.js",
|
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@ripple-ts/adapter": "0.2.
|
|
18
|
+
"@ripple-ts/adapter": "0.2.214"
|
|
19
19
|
},
|
|
20
|
-
"
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "^1.3.9"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://ripple-ts.com",
|
|
21
24
|
"repository": {
|
|
22
25
|
"type": "git",
|
|
23
26
|
"url": "git+https://github.com/Ripple-TS/ripple.git",
|
package/src/index.js
CHANGED
|
@@ -4,13 +4,46 @@
|
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_HOSTNAME,
|
|
6
6
|
DEFAULT_PORT,
|
|
7
|
+
DEFAULT_STATIC_PREFIX,
|
|
8
|
+
DEFAULT_STATIC_MAX_AGE,
|
|
9
|
+
get_mime_type,
|
|
10
|
+
get_static_cache_control,
|
|
7
11
|
internal_server_error_response,
|
|
8
12
|
run_next_middleware,
|
|
9
|
-
serveStatic as create_static_handler,
|
|
10
13
|
} from '@ripple-ts/adapter';
|
|
14
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
15
|
+
import { resolve, sep } from 'node:path';
|
|
11
16
|
|
|
12
17
|
/** @typedef {import('@ripple-ts/adapter').ServeStaticDirectoryOptions} StaticServeOptions */
|
|
13
18
|
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Runtime primitives — platform-specific capabilities for Ripple's server runtime
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bun runtime primitives for the Ripple adapter contract.
|
|
25
|
+
*
|
|
26
|
+
* Provides:
|
|
27
|
+
* - `hash`: SHA-256 hex digest truncated to 8 chars via Bun.CryptoHasher
|
|
28
|
+
* - `createAsyncContext`: AsyncLocalStorage-backed request-scoped context
|
|
29
|
+
*
|
|
30
|
+
* @type {import('@ripple-ts/adapter').RuntimePrimitives}
|
|
31
|
+
*/
|
|
32
|
+
export const runtime = {
|
|
33
|
+
hash(str) {
|
|
34
|
+
const hasher = new globalThis.Bun.CryptoHasher('sha256');
|
|
35
|
+
hasher.update(str);
|
|
36
|
+
return hasher.digest('hex').slice(0, 8);
|
|
37
|
+
},
|
|
38
|
+
createAsyncContext() {
|
|
39
|
+
const als = new AsyncLocalStorage();
|
|
40
|
+
return {
|
|
41
|
+
run: (store, fn) => als.run(store, fn),
|
|
42
|
+
getStore: () => als.getStore(),
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
14
47
|
/**
|
|
15
48
|
* @typedef {{
|
|
16
49
|
* port?: number,
|
|
@@ -40,7 +73,7 @@ export function serve(fetch_handler, options = {}) {
|
|
|
40
73
|
/** @type {ReturnType<typeof serveStatic> | null} */
|
|
41
74
|
let static_middleware = null;
|
|
42
75
|
if (static_options !== false) {
|
|
43
|
-
const { dir = '
|
|
76
|
+
const { dir = '.', ...static_handler_options } = static_options;
|
|
44
77
|
static_middleware = serveStatic(dir, static_handler_options);
|
|
45
78
|
}
|
|
46
79
|
|
|
@@ -104,16 +137,63 @@ export function serve(fetch_handler, options = {}) {
|
|
|
104
137
|
* @returns {(request: Request, server: Server, next: () => Promise<Response>) => Promise<Response>}
|
|
105
138
|
*/
|
|
106
139
|
export function serveStatic(dir, options = {}) {
|
|
107
|
-
const
|
|
140
|
+
const {
|
|
141
|
+
prefix = DEFAULT_STATIC_PREFIX,
|
|
142
|
+
maxAge = DEFAULT_STATIC_MAX_AGE,
|
|
143
|
+
immutable = false,
|
|
144
|
+
} = options;
|
|
145
|
+
|
|
146
|
+
const base_dir = resolve(dir);
|
|
108
147
|
|
|
109
148
|
return async function static_middleware(request, server, next) {
|
|
110
149
|
void server;
|
|
111
150
|
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
return
|
|
151
|
+
const request_method = (request.method || 'GET').toUpperCase();
|
|
152
|
+
if (request_method !== 'GET' && request_method !== 'HEAD') {
|
|
153
|
+
return await next();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let pathname;
|
|
157
|
+
try {
|
|
158
|
+
pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname);
|
|
159
|
+
} catch {
|
|
160
|
+
return await next();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!pathname.startsWith(prefix)) {
|
|
164
|
+
return await next();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pathname = pathname.slice(prefix.length) || '/';
|
|
168
|
+
if (!pathname.startsWith('/')) {
|
|
169
|
+
pathname = '/' + pathname;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const file_path = resolve(base_dir, `.${pathname}`);
|
|
173
|
+
const is_within_base_dir = file_path === base_dir || file_path.startsWith(base_dir + sep);
|
|
174
|
+
if (!is_within_base_dir) {
|
|
175
|
+
return await next();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const bun_file = globalThis.Bun.file(file_path);
|
|
179
|
+
if (!(await bun_file.exists())) {
|
|
180
|
+
return await next();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Bun.file().size is 0 for directories; skip them
|
|
184
|
+
if (bun_file.size === 0) {
|
|
185
|
+
return await next();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const headers = new Headers();
|
|
189
|
+
headers.set('Content-Type', get_mime_type(file_path));
|
|
190
|
+
headers.set('Content-Length', String(bun_file.size));
|
|
191
|
+
headers.set('Cache-Control', get_static_cache_control(pathname, maxAge, immutable));
|
|
192
|
+
|
|
193
|
+
if (request_method === 'HEAD') {
|
|
194
|
+
return new Response(null, { status: 200, headers });
|
|
115
195
|
}
|
|
116
196
|
|
|
117
|
-
return
|
|
197
|
+
return new Response(bun_file, { status: 200, headers });
|
|
118
198
|
};
|
|
119
199
|
}
|
package/tests/serve.test.js
CHANGED
|
@@ -1,17 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
2
10
|
import { tmpdir } from 'node:os';
|
|
3
11
|
import { join } from 'node:path';
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
13
|
import { serve, serveStatic } from '../src/index.js';
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
const original_bun = Reflect.get(globalThis, 'Bun');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a mock for Bun.file() that uses node:fs under the hood.
|
|
19
|
+
* Returns a Blob-like object so `new Response(bun_file)` works correctly.
|
|
20
|
+
*
|
|
21
|
+
* @param {string | URL} file_path
|
|
22
|
+
*/
|
|
23
|
+
function mock_bun_file(file_path) {
|
|
24
|
+
file_path = String(file_path);
|
|
25
|
+
const file_exists = existsSync(file_path);
|
|
26
|
+
const stats = file_exists ? statSync(file_path) : null;
|
|
27
|
+
const is_dir = stats?.isDirectory() ?? false;
|
|
28
|
+
|
|
29
|
+
if (!file_exists || is_dir) {
|
|
30
|
+
return {
|
|
31
|
+
exists: async () => false,
|
|
32
|
+
size: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const content = readFileSync(file_path);
|
|
37
|
+
const blob = new Blob([content]);
|
|
38
|
+
|
|
39
|
+
// Attach Bun.file-specific methods onto the Blob so it can act as both
|
|
40
|
+
// a BodyInit (Blob) and a BunFile (exists/size).
|
|
41
|
+
const bun_file = Object.assign(blob, {
|
|
42
|
+
exists: /** @returns {Promise<boolean>} */ async () => true,
|
|
43
|
+
});
|
|
44
|
+
const file_size = /** @type {import('node:fs').Stats} */ (stats).size;
|
|
45
|
+
Object.defineProperty(bun_file, 'size', { value: file_size });
|
|
46
|
+
return bun_file;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ensure globalThis.Bun has at least the `file` mock.
|
|
51
|
+
*/
|
|
52
|
+
function ensure_bun_file_mock() {
|
|
53
|
+
const bun = Reflect.get(globalThis, 'Bun');
|
|
54
|
+
if (!bun) {
|
|
55
|
+
Object.defineProperty(globalThis, 'Bun', {
|
|
56
|
+
value: { file: mock_bun_file },
|
|
57
|
+
writable: true,
|
|
58
|
+
configurable: true,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!bun.file) {
|
|
63
|
+
Object.defineProperty(bun, 'file', {
|
|
64
|
+
value: mock_bun_file,
|
|
65
|
+
writable: true,
|
|
66
|
+
configurable: true,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
ensure_bun_file_mock();
|
|
73
|
+
});
|
|
9
74
|
|
|
10
75
|
afterEach(() => {
|
|
11
76
|
if (original_bun === undefined) {
|
|
12
77
|
Reflect.deleteProperty(globalThis, 'Bun');
|
|
13
78
|
} else {
|
|
14
|
-
globalThis
|
|
79
|
+
Object.defineProperty(globalThis, 'Bun', {
|
|
80
|
+
value: original_bun,
|
|
81
|
+
writable: true,
|
|
82
|
+
configurable: true,
|
|
83
|
+
});
|
|
15
84
|
}
|
|
16
85
|
vi.restoreAllMocks();
|
|
17
86
|
});
|
|
@@ -33,9 +102,11 @@ function create_bun_mock() {
|
|
|
33
102
|
return server;
|
|
34
103
|
});
|
|
35
104
|
|
|
36
|
-
globalThis
|
|
37
|
-
serve: serve_spy,
|
|
38
|
-
|
|
105
|
+
Object.defineProperty(globalThis, 'Bun', {
|
|
106
|
+
value: { serve: serve_spy, file: mock_bun_file },
|
|
107
|
+
writable: true,
|
|
108
|
+
configurable: true,
|
|
109
|
+
});
|
|
39
110
|
|
|
40
111
|
return {
|
|
41
112
|
serve_spy,
|
|
@@ -165,7 +236,7 @@ describe('@ripple-ts/adapter-bun serve()', () => {
|
|
|
165
236
|
|
|
166
237
|
const response = await static_middleware(
|
|
167
238
|
new Request('http://localhost/assets/app.js'),
|
|
168
|
-
/** @type {
|
|
239
|
+
/** @type {import('bun').Server<undefined>} */ ({}),
|
|
169
240
|
next,
|
|
170
241
|
);
|
|
171
242
|
|
|
@@ -186,7 +257,7 @@ describe('@ripple-ts/adapter-bun serve()', () => {
|
|
|
186
257
|
|
|
187
258
|
const response = await static_middleware(
|
|
188
259
|
new Request('http://localhost/assets/missing.js'),
|
|
189
|
-
/** @type {
|
|
260
|
+
/** @type {import('bun').Server<undefined>} */ ({}),
|
|
190
261
|
next,
|
|
191
262
|
);
|
|
192
263
|
|
|
@@ -197,12 +268,10 @@ describe('@ripple-ts/adapter-bun serve()', () => {
|
|
|
197
268
|
}
|
|
198
269
|
});
|
|
199
270
|
|
|
200
|
-
it('serves files from ./
|
|
201
|
-
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-bun-default-static-'));
|
|
271
|
+
it('serves files from ./ by default', async () => {
|
|
272
|
+
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-bun-default-static-dir'));
|
|
202
273
|
try {
|
|
203
|
-
|
|
204
|
-
mkdirSync(public_dir);
|
|
205
|
-
writeFileSync(join(public_dir, 'llms.txt'), 'hello llms');
|
|
274
|
+
writeFileSync(join(temp_dir, 'llms.txt'), 'hello llms');
|
|
206
275
|
|
|
207
276
|
await with_cwd(temp_dir, async () => {
|
|
208
277
|
const { server, get_fetch } = create_bun_mock();
|
|
@@ -221,11 +290,9 @@ describe('@ripple-ts/adapter-bun serve()', () => {
|
|
|
221
290
|
});
|
|
222
291
|
|
|
223
292
|
it('can disable default static serving via options.static = false', async () => {
|
|
224
|
-
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-bun-default-static-disabled-'));
|
|
293
|
+
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-bun-default-static-disabled-dir'));
|
|
225
294
|
try {
|
|
226
|
-
|
|
227
|
-
mkdirSync(public_dir);
|
|
228
|
-
writeFileSync(join(public_dir, 'llms.txt'), 'hello llms');
|
|
295
|
+
writeFileSync(join(temp_dir, 'llms.txt'), 'hello llms');
|
|
229
296
|
|
|
230
297
|
await with_cwd(temp_dir, async () => {
|
|
231
298
|
const { server, get_fetch } = create_bun_mock();
|
package/types/index.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AdapterCoreOptions,
|
|
3
3
|
NextMiddleware,
|
|
4
|
+
RuntimePrimitives,
|
|
4
5
|
ServeFunction,
|
|
5
6
|
ServeStaticOptions as BaseServeStaticOptions,
|
|
6
7
|
ServeStaticDirectoryOptions as BaseServeStaticDirectoryOptions,
|
|
7
8
|
} from '@ripple-ts/adapter';
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Bun runtime primitives for the Ripple adapter contract.
|
|
12
|
+
*
|
|
13
|
+
* - `hash`: SHA-256 (truncated to 8 hex chars) via `Bun.CryptoHasher`
|
|
14
|
+
* - `createAsyncContext`: `AsyncLocalStorage` from `node:async_hooks` (Bun-compatible)
|
|
15
|
+
*/
|
|
16
|
+
export const runtime: RuntimePrimitives;
|
|
17
|
+
|
|
9
18
|
export type ServeOptions = AdapterCoreOptions & {
|
|
10
19
|
middleware?: NextMiddleware<Request, any> | null;
|
|
11
20
|
static?: BaseServeStaticDirectoryOptions | false;
|