@ripple-ts/adapter-bun 0.2.213 → 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 CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## 0.2.213
4
11
 
5
12
  ### 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.213",
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.213"
18
+ "@ripple-ts/adapter": "0.2.214"
19
19
  },
20
- "homepage": "https://ripplejs.com",
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 = 'public', ...static_handler_options } = static_options;
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 serve_static_request = create_static_handler(dir, options);
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 response = serve_static_request(request);
113
- if (response !== null) {
114
- return response;
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 await next();
197
+ return new Response(bun_file, { status: 200, headers });
118
198
  };
119
199
  }
@@ -1,17 +1,86 @@
1
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
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
- /** @type {any} */
8
- const original_bun = globalThis.Bun;
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.Bun = original_bun;
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.Bun = {
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 {any} */ ({}),
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 {any} */ ({}),
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 ./public by default', async () => {
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
- const public_dir = join(temp_dir, 'public');
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
- const public_dir = join(temp_dir, 'public');
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;