@ripple-ts/adapter-bun 0.2.208

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,78 @@
1
+ # @ripple-ts/adapter-bun
2
+
3
+ Bun adapter for Ripple metaframework apps.
4
+
5
+ It exposes the same `serve(fetch_handler, options?)` contract as
6
+ `@ripple-ts/adapter-node`, backed by `Bun.serve`.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pnpm add @ripple-ts/adapter-bun
12
+ # or
13
+ npm install @ripple-ts/adapter-bun
14
+ # or
15
+ yarn add @ripple-ts/adapter-bun
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```js
21
+ import { serve } from '@ripple-ts/adapter-bun';
22
+
23
+ const app = serve(async (request) => {
24
+ const url = new URL(request.url);
25
+
26
+ if (url.pathname === '/health') {
27
+ return new Response('ok');
28
+ }
29
+
30
+ return new Response('Hello from Ripple adapter-bun!', {
31
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
32
+ });
33
+ });
34
+
35
+ app.listen(3000);
36
+ ```
37
+
38
+ ## API
39
+
40
+ ### `serve(fetch_handler, options?)`
41
+
42
+ - `fetch_handler`:
43
+ `(request: Request, platform?: any) => Response | Promise<Response>`
44
+ - `options.port` (default: `3000`)
45
+ - `options.hostname` (default: `localhost`)
46
+ - `options.static` (default: `{ dir: 'public' }`): serves static files before
47
+ middleware/handler
48
+ - `options.static.dir` (default: `public`, resolved from `process.cwd()`)
49
+ - `options.static.prefix` (default: `/`)
50
+ - `options.static.maxAge` (default: `86400`)
51
+ - `options.static.immutable` (default: `false`)
52
+ - set `options.static = false` to disable automatic static serving
53
+ - `options.middleware` (optional):
54
+ `(request, server, next) => Response | Promise<Response> | void`
55
+
56
+ Returns:
57
+
58
+ - `listen(port?)`: starts Bun server and returns the Bun server instance
59
+ - `close()`: stops the current Bun server instance
60
+
61
+ ### `serveStatic(dir, options?)`
62
+
63
+ Creates a Bun middleware that serves static assets from `dir`.
64
+
65
+ - `options.prefix` (default: `/`)
66
+ - `options.maxAge` (default: `86400`)
67
+ - `options.immutable` (default: `false`)
68
+
69
+ ## Notes
70
+
71
+ - Requires Bun runtime (`Bun.serve`).
72
+ - Static file logic and MIME type mappings are shared from `@ripple-ts/adapter`.
73
+ - `platform` passed to `fetch_handler` contains `{ bun_server }`.
74
+ - Unhandled errors return `500 Internal Server Error`.
75
+
76
+ ## License
77
+
78
+ MIT
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@ripple-ts/adapter-bun",
3
+ "description": "Bun adapter for Ripple metaframework (Web Request/Response bridge)",
4
+ "license": "MIT",
5
+ "author": "Dominic Gannaway",
6
+ "version": "0.2.208",
7
+ "type": "module",
8
+ "module": "src/index.js",
9
+ "main": "src/index.js",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./types/index.d.ts",
13
+ "import": "./src/index.js",
14
+ "default": "./src/index.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "test": "pnpm -w test --project adapter-bun"
19
+ },
20
+ "dependencies": {
21
+ "@ripple-ts/adapter": "workspace:*"
22
+ },
23
+ "homepage": "https://ripplejs.com",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/Ripple-TS/ripple.git",
27
+ "directory": "packages/adapter-bun"
28
+ },
29
+ "peerDependencies": {
30
+ "bun": "^1.0.0"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/Ripple-TS/ripple/issues"
34
+ }
35
+ }
package/src/index.js ADDED
@@ -0,0 +1,119 @@
1
+ /** @typedef {typeof import('bun')} Bun */
2
+ /** @typedef {Bun.Server<undefined>} Server */
3
+
4
+ import {
5
+ DEFAULT_HOSTNAME,
6
+ DEFAULT_PORT,
7
+ internal_server_error_response,
8
+ run_next_middleware,
9
+ serveStatic as create_static_handler,
10
+ } from '@ripple-ts/adapter';
11
+
12
+ /** @typedef {import('@ripple-ts/adapter').ServeStaticDirectoryOptions} StaticServeOptions */
13
+
14
+ /**
15
+ * @typedef {{
16
+ * port?: number,
17
+ * hostname?: string,
18
+ * middleware?: ((
19
+ * request: Request,
20
+ * server: Server,
21
+ * next: () => Promise<Response>
22
+ * ) => Response | Promise<Response> | void) | null,
23
+ * static?: StaticServeOptions | false,
24
+ * }} ServeOptions
25
+ */
26
+
27
+ /**
28
+ * @param {(request: Request, platform?: any) => Response | Promise<Response>} fetch_handler
29
+ * @param {ServeOptions} [options]
30
+ * @returns {{ listen: (port?: number) => Server, close: () => void }}
31
+ */
32
+ export function serve(fetch_handler, options = {}) {
33
+ const {
34
+ port = DEFAULT_PORT,
35
+ hostname = DEFAULT_HOSTNAME,
36
+ middleware = null,
37
+ static: static_options = {},
38
+ } = options;
39
+
40
+ /** @type {ReturnType<typeof serveStatic> | null} */
41
+ let static_middleware = null;
42
+ if (static_options !== false) {
43
+ const { dir = 'public', ...static_handler_options } = static_options;
44
+ static_middleware = serveStatic(dir, static_handler_options);
45
+ }
46
+
47
+ /** @type {Server | null} */
48
+ let bun_server = null;
49
+
50
+ return {
51
+ listen(listen_port = port) {
52
+ /** @type {typeof import('bun')} */
53
+ const bun = globalThis.Bun;
54
+ if (bun == null || typeof bun.serve !== 'function') {
55
+ throw new Error('@ripple-ts/adapter-bun requires Bun runtime');
56
+ }
57
+
58
+ bun_server = bun.serve({
59
+ port: listen_port,
60
+ hostname,
61
+ async fetch(request, server) {
62
+ const platform = { bun_server: server };
63
+ try {
64
+ const run_fetch_handler = async () => {
65
+ return await fetch_handler(request, platform);
66
+ };
67
+
68
+ const run_app_middleware = async () => {
69
+ if (middleware !== null) {
70
+ return await run_next_middleware(middleware, request, server, run_fetch_handler);
71
+ }
72
+
73
+ return await run_fetch_handler();
74
+ };
75
+
76
+ if (static_middleware !== null) {
77
+ return await run_next_middleware(static_middleware, request, server, async () => {
78
+ return await run_app_middleware();
79
+ });
80
+ }
81
+
82
+ return await run_app_middleware();
83
+ } catch {
84
+ return internal_server_error_response();
85
+ }
86
+ },
87
+ });
88
+
89
+ return bun_server;
90
+ },
91
+ close() {
92
+ if (bun_server && typeof bun_server.stop === 'function') {
93
+ bun_server.stop();
94
+ }
95
+ },
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Create a Bun middleware that serves static files from a directory
101
+ *
102
+ * @param {string} dir - Directory to serve files from (relative to cwd or absolute)
103
+ * @param {import('@ripple-ts/adapter').ServeStaticOptions} [options]
104
+ * @returns {(request: Request, server: Server, next: () => Promise<Response>) => Promise<Response>}
105
+ */
106
+ export function serveStatic(dir, options = {}) {
107
+ const serve_static_request = create_static_handler(dir, options);
108
+
109
+ return async function static_middleware(request, server, next) {
110
+ void server;
111
+
112
+ const response = serve_static_request(request);
113
+ if (response !== null) {
114
+ return response;
115
+ }
116
+
117
+ return await next();
118
+ };
119
+ }
@@ -0,0 +1,272 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { serve, serveStatic } from '../src/index.js';
6
+
7
+ /** @type {any} */
8
+ const original_bun = globalThis.Bun;
9
+
10
+ afterEach(() => {
11
+ if (original_bun === undefined) {
12
+ Reflect.deleteProperty(globalThis, 'Bun');
13
+ } else {
14
+ globalThis.Bun = original_bun;
15
+ }
16
+ vi.restoreAllMocks();
17
+ });
18
+
19
+ /**
20
+ * @returns {{
21
+ * serve_spy: import('vitest').Mock,
22
+ * server: { stop: import('vitest').Mock },
23
+ * get_fetch: () => (request: Request, server: any) => Promise<Response>
24
+ * }}
25
+ */
26
+ function create_bun_mock() {
27
+ /** @type {(request: Request, server: any) => Promise<Response>} */
28
+ let fetch_handler = async () => new Response('not-configured', { status: 500 });
29
+
30
+ const server = { stop: vi.fn() };
31
+ const serve_spy = vi.fn((options) => {
32
+ fetch_handler = options.fetch;
33
+ return server;
34
+ });
35
+
36
+ globalThis.Bun = {
37
+ serve: serve_spy,
38
+ };
39
+
40
+ return {
41
+ serve_spy,
42
+ server,
43
+ get_fetch() {
44
+ return fetch_handler;
45
+ },
46
+ };
47
+ }
48
+
49
+ /**
50
+ * @param {string} cwd
51
+ * @param {() => Promise<void>} run
52
+ * @returns {Promise<void>}
53
+ */
54
+ async function with_cwd(cwd, run) {
55
+ const previous_cwd = process.cwd();
56
+ process.chdir(cwd);
57
+ try {
58
+ await run();
59
+ } finally {
60
+ process.chdir(previous_cwd);
61
+ }
62
+ }
63
+
64
+ describe('@ripple-ts/adapter-bun serve()', () => {
65
+ it('throws when Bun runtime is unavailable', () => {
66
+ Reflect.deleteProperty(globalThis, 'Bun');
67
+
68
+ const app = serve(() => new Response('ok'));
69
+ expect(() => app.listen()).toThrow('@ripple-ts/adapter-bun requires Bun runtime');
70
+ });
71
+
72
+ it('starts bun server and forwards request to fetch handler', async () => {
73
+ const { serve_spy, server, get_fetch } = create_bun_mock();
74
+ const fetch_handler = vi.fn(() => new Response('ok'));
75
+
76
+ const app = serve(fetch_handler);
77
+ const returned_server = app.listen();
78
+
79
+ expect(returned_server).toBe(server);
80
+ expect(serve_spy).toHaveBeenCalledTimes(1);
81
+ expect(serve_spy).toHaveBeenCalledWith(expect.objectContaining({ port: 3000 }));
82
+ expect(serve_spy).toHaveBeenCalledWith(expect.objectContaining({ hostname: 'localhost' }));
83
+
84
+ const request = new Request('http://localhost/users');
85
+ const response = await get_fetch()(request, server);
86
+
87
+ expect(fetch_handler).toHaveBeenCalledTimes(1);
88
+ expect(fetch_handler).toHaveBeenCalledWith(request, { bun_server: server });
89
+ expect(await response.text()).toBe('ok');
90
+ });
91
+
92
+ it('uses explicit listen port over default option port', () => {
93
+ const { serve_spy } = create_bun_mock();
94
+
95
+ const app = serve(() => new Response('ok'), { port: 8080, hostname: '0.0.0.0' });
96
+ app.listen(9090);
97
+
98
+ expect(serve_spy).toHaveBeenCalledWith(expect.objectContaining({ port: 9090 }));
99
+ expect(serve_spy).toHaveBeenCalledWith(expect.objectContaining({ hostname: '0.0.0.0' }));
100
+ });
101
+
102
+ it('supports middleware short-circuit responses', async () => {
103
+ const { server, get_fetch } = create_bun_mock();
104
+ const fetch_handler = vi.fn(() => new Response('handler'));
105
+ const middleware = vi.fn(() => new Response('middleware', { status: 202 }));
106
+
107
+ const app = serve(fetch_handler, { middleware });
108
+ app.listen();
109
+
110
+ const response = await get_fetch()(new Request('http://localhost/'), server);
111
+ expect(middleware).toHaveBeenCalledTimes(1);
112
+ expect(fetch_handler).not.toHaveBeenCalled();
113
+ expect(response.status).toBe(202);
114
+ expect(await response.text()).toBe('middleware');
115
+ });
116
+
117
+ it('supports middleware next() flow', async () => {
118
+ const { server, get_fetch } = create_bun_mock();
119
+ const fetch_handler = vi.fn(() => new Response('handler'));
120
+ const middleware = vi.fn(async (request, bun_server, next) => {
121
+ void request;
122
+ void bun_server;
123
+ return await next();
124
+ });
125
+
126
+ const app = serve(fetch_handler, { middleware });
127
+ app.listen();
128
+
129
+ const response = await get_fetch()(new Request('http://localhost/'), server);
130
+ expect(middleware).toHaveBeenCalledTimes(1);
131
+ expect(fetch_handler).toHaveBeenCalledTimes(1);
132
+ expect(await response.text()).toBe('handler');
133
+ });
134
+
135
+ it('returns 500 response when fetch handler throws', async () => {
136
+ const { server, get_fetch } = create_bun_mock();
137
+ const app = serve(() => {
138
+ throw new Error('boom');
139
+ });
140
+ app.listen();
141
+
142
+ const response = await get_fetch()(new Request('http://localhost/'), server);
143
+ expect(response.status).toBe(500);
144
+ expect(response.headers.get('content-type')).toBe('text/plain; charset=utf-8');
145
+ expect(await response.text()).toBe('Internal Server Error');
146
+ });
147
+
148
+ it('stops server on close()', () => {
149
+ const { server } = create_bun_mock();
150
+
151
+ const app = serve(() => new Response('ok'));
152
+ app.listen();
153
+ app.close();
154
+
155
+ expect(server.stop).toHaveBeenCalledTimes(1);
156
+ });
157
+
158
+ it('serveStatic middleware serves matching files', async () => {
159
+ const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-bun-static-'));
160
+ try {
161
+ writeFileSync(join(temp_dir, 'app.js'), 'console.log("bun");');
162
+
163
+ const static_middleware = serveStatic(temp_dir, { prefix: '/assets' });
164
+ const next = vi.fn(async () => new Response('next'));
165
+
166
+ const response = await static_middleware(
167
+ new Request('http://localhost/assets/app.js'),
168
+ /** @type {any} */ ({}),
169
+ next,
170
+ );
171
+
172
+ expect(next).not.toHaveBeenCalled();
173
+ expect(response.status).toBe(200);
174
+ expect(response.headers.get('content-type')).toBe('text/javascript; charset=utf-8');
175
+ expect(await response.text()).toContain('console.log');
176
+ } finally {
177
+ rmSync(temp_dir, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ it('serveStatic middleware falls through when no file is found', async () => {
182
+ const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-bun-static-fallthrough-'));
183
+ try {
184
+ const static_middleware = serveStatic(temp_dir, { prefix: '/assets' });
185
+ const next = vi.fn(async () => new Response('next'));
186
+
187
+ const response = await static_middleware(
188
+ new Request('http://localhost/assets/missing.js'),
189
+ /** @type {any} */ ({}),
190
+ next,
191
+ );
192
+
193
+ expect(next).toHaveBeenCalledTimes(1);
194
+ expect(await response.text()).toBe('next');
195
+ } finally {
196
+ rmSync(temp_dir, { recursive: true, force: true });
197
+ }
198
+ });
199
+
200
+ it('serves files from ./public by default', async () => {
201
+ const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-bun-default-static-'));
202
+ try {
203
+ const public_dir = join(temp_dir, 'public');
204
+ mkdirSync(public_dir);
205
+ writeFileSync(join(public_dir, 'llms.txt'), 'hello llms');
206
+
207
+ await with_cwd(temp_dir, async () => {
208
+ const { server, get_fetch } = create_bun_mock();
209
+ const fetch_handler = vi.fn(() => new Response('fallback', { status: 404 }));
210
+ const app = serve(fetch_handler);
211
+ app.listen();
212
+
213
+ const response = await get_fetch()(new Request('http://localhost/llms.txt'), server);
214
+ expect(response.status).toBe(200);
215
+ expect(await response.text()).toBe('hello llms');
216
+ expect(fetch_handler).not.toHaveBeenCalled();
217
+ });
218
+ } finally {
219
+ rmSync(temp_dir, { recursive: true, force: true });
220
+ }
221
+ });
222
+
223
+ it('can disable default static serving via options.static = false', async () => {
224
+ const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-bun-default-static-disabled-'));
225
+ try {
226
+ const public_dir = join(temp_dir, 'public');
227
+ mkdirSync(public_dir);
228
+ writeFileSync(join(public_dir, 'llms.txt'), 'hello llms');
229
+
230
+ await with_cwd(temp_dir, async () => {
231
+ const { server, get_fetch } = create_bun_mock();
232
+ const fetch_handler = vi.fn(() => new Response('fallback', { status: 404 }));
233
+ const app = serve(fetch_handler, { static: false });
234
+ app.listen();
235
+
236
+ const response = await get_fetch()(new Request('http://localhost/llms.txt'), server);
237
+ expect(response.status).toBe(404);
238
+ expect(await response.text()).toBe('fallback');
239
+ expect(fetch_handler).toHaveBeenCalledTimes(1);
240
+ });
241
+ } finally {
242
+ rmSync(temp_dir, { recursive: true, force: true });
243
+ }
244
+ });
245
+
246
+ it('serves static files via options.static with custom prefix and cache settings', async () => {
247
+ const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-bun-static-options-'));
248
+ try {
249
+ writeFileSync(join(temp_dir, 'asset.txt'), 'asset-data');
250
+
251
+ const { server, get_fetch } = create_bun_mock();
252
+ const fetch_handler = vi.fn(() => new Response('fallback', { status: 404 }));
253
+ const app = serve(fetch_handler, {
254
+ static: {
255
+ dir: temp_dir,
256
+ prefix: '/public',
257
+ maxAge: 60,
258
+ immutable: true,
259
+ },
260
+ });
261
+ app.listen();
262
+
263
+ const response = await get_fetch()(new Request('http://localhost/public/asset.txt'), server);
264
+ expect(response.status).toBe(200);
265
+ expect(response.headers.get('cache-control')).toBe('public, max-age=31536000, immutable');
266
+ expect(await response.text()).toBe('asset-data');
267
+ expect(fetch_handler).not.toHaveBeenCalled();
268
+ } finally {
269
+ rmSync(temp_dir, { recursive: true, force: true });
270
+ }
271
+ });
272
+ });
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ const types_source = readFileSync(new URL('../types/index.d.ts', import.meta.url), 'utf8');
5
+
6
+ describe('@ripple-ts/adapter-bun types', () => {
7
+ it('uses shared ServeStaticDirectoryOptions alias from @ripple-ts/adapter', () => {
8
+ expect(types_source).toContain(
9
+ 'ServeStaticDirectoryOptions as BaseServeStaticDirectoryOptions',
10
+ );
11
+ expect(types_source).toContain('static?: BaseServeStaticDirectoryOptions | false;');
12
+ expect(types_source).toContain('export type ServeStaticOptions = BaseServeStaticOptions;');
13
+ });
14
+
15
+ it('does not inline static dir type shape locally', () => {
16
+ expect(types_source).not.toMatch(/BaseServeStaticOptions\s*&\s*\{\s*dir\?: string;\s*\}/);
17
+ });
18
+ });
@@ -0,0 +1,25 @@
1
+ import type {
2
+ AdapterCoreOptions,
3
+ FetchHandler,
4
+ NextMiddleware,
5
+ ServeStaticOptions as BaseServeStaticOptions,
6
+ ServeStaticDirectoryOptions as BaseServeStaticDirectoryOptions,
7
+ ServeResult,
8
+ } from '@ripple-ts/adapter';
9
+
10
+ export type ServeOptions = AdapterCoreOptions & {
11
+ middleware?: NextMiddleware<Request, any> | null;
12
+ static?: BaseServeStaticDirectoryOptions | false;
13
+ };
14
+
15
+ export type ServeStaticOptions = BaseServeStaticOptions;
16
+
17
+ export function serve(
18
+ fetch_handler: FetchHandler<{ bun_server: any }>,
19
+ options?: ServeOptions,
20
+ ): ServeResult<any>;
21
+
22
+ export function serveStatic(
23
+ dir: string,
24
+ options?: ServeStaticOptions,
25
+ ): NextMiddleware<Request, any, Response>;