@ripple-ts/adapter 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 +33 -0
- package/package.json +29 -0
- package/src/index.js +202 -0
- package/tests/index.test.js +192 -0
- package/tests/types.test.js +18 -0
- package/types/index.d.ts +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @ripple-ts/adapter
|
|
2
|
+
|
|
3
|
+
Shared adapter primitives for Ripple metaframework adapters.
|
|
4
|
+
|
|
5
|
+
This package contains common runtime helpers and shared type contracts used by
|
|
6
|
+
environment-specific adapters like:
|
|
7
|
+
|
|
8
|
+
- `@ripple-ts/adapter-node`
|
|
9
|
+
- `@ripple-ts/adapter-bun`
|
|
10
|
+
|
|
11
|
+
## Exports
|
|
12
|
+
|
|
13
|
+
- `DEFAULT_PORT`
|
|
14
|
+
- `DEFAULT_HOSTNAME`
|
|
15
|
+
- `internal_server_error_response()`
|
|
16
|
+
- `run_next_middleware()`
|
|
17
|
+
- `serveStatic()`
|
|
18
|
+
- `MIME_TYPES`
|
|
19
|
+
- `get_mime_type()`
|
|
20
|
+
- `is_hashed_asset()`
|
|
21
|
+
- `get_static_cache_control()`
|
|
22
|
+
|
|
23
|
+
Type exports:
|
|
24
|
+
|
|
25
|
+
- `FetchHandler`
|
|
26
|
+
- `AdapterCoreOptions`
|
|
27
|
+
- `NextMiddleware`
|
|
28
|
+
- `ServeResult`
|
|
29
|
+
- `ServeStaticOptions`
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ripple-ts/adapter",
|
|
3
|
+
"description": "Shared adapter primitives for Ripple metaframework adapters",
|
|
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"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://ripplejs.com",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Ripple-TS/ripple.git",
|
|
24
|
+
"directory": "packages/adapter"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/Ripple-TS/ripple/issues"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// NOTE: For now we'd not have it fully runtime-agnostic
|
|
2
|
+
// When we'd like to move away from Node.js modules or explore other runtime options.
|
|
3
|
+
import { createReadStream, existsSync, statSync } from 'node:fs';
|
|
4
|
+
import { extname, resolve, sep } from 'node:path';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_HOSTNAME = 'localhost';
|
|
8
|
+
export const DEFAULT_PORT = 3000;
|
|
9
|
+
export const DEFAULT_STATIC_PREFIX = '/';
|
|
10
|
+
export const DEFAULT_STATIC_MAX_AGE = 86400;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Common MIME types for static files
|
|
14
|
+
* @type {Readonly<Record<string, string>>}
|
|
15
|
+
*/
|
|
16
|
+
export const MIME_TYPES = {
|
|
17
|
+
'.html': 'text/html; charset=utf-8',
|
|
18
|
+
'.css': 'text/css; charset=utf-8',
|
|
19
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
20
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
21
|
+
'.json': 'application/json; charset=utf-8',
|
|
22
|
+
'.png': 'image/png',
|
|
23
|
+
'.jpg': 'image/jpeg',
|
|
24
|
+
'.jpeg': 'image/jpeg',
|
|
25
|
+
'.gif': 'image/gif',
|
|
26
|
+
'.svg': 'image/svg+xml',
|
|
27
|
+
'.ico': 'image/x-icon',
|
|
28
|
+
'.woff': 'font/woff',
|
|
29
|
+
'.woff2': 'font/woff2',
|
|
30
|
+
'.ttf': 'font/ttf',
|
|
31
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
32
|
+
'.otf': 'font/otf',
|
|
33
|
+
'.webp': 'image/webp',
|
|
34
|
+
'.avif': 'image/avif',
|
|
35
|
+
'.mp4': 'video/mp4',
|
|
36
|
+
'.webm': 'video/webm',
|
|
37
|
+
'.mp3': 'audio/mpeg',
|
|
38
|
+
'.wav': 'audio/wav',
|
|
39
|
+
'.pdf': 'application/pdf',
|
|
40
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
41
|
+
'.xml': 'application/xml',
|
|
42
|
+
'.wasm': 'application/wasm',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {Response}
|
|
47
|
+
*/
|
|
48
|
+
export function internal_server_error_response() {
|
|
49
|
+
return new Response('Internal Server Error', {
|
|
50
|
+
status: 500,
|
|
51
|
+
headers: {
|
|
52
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @template RequestValue
|
|
59
|
+
* @template Server
|
|
60
|
+
* @template ResultValue
|
|
61
|
+
* @param {(request: RequestValue, server: Server, next: () => Promise<ResultValue>) => ResultValue | Promise<ResultValue> | void} middleware
|
|
62
|
+
* @param {RequestValue} request
|
|
63
|
+
* @param {Server} server
|
|
64
|
+
* @param {() => Promise<ResultValue>} next_handler
|
|
65
|
+
* @returns {Promise<ResultValue>}
|
|
66
|
+
*/
|
|
67
|
+
export async function run_next_middleware(middleware, request, server, next_handler) {
|
|
68
|
+
/** @type {Promise<ResultValue> | null} */
|
|
69
|
+
let next_promise = null;
|
|
70
|
+
|
|
71
|
+
const next = () => {
|
|
72
|
+
if (next_promise === null) {
|
|
73
|
+
next_promise = Promise.resolve().then(next_handler);
|
|
74
|
+
}
|
|
75
|
+
return next_promise;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const middleware_result = await middleware(request, server, next);
|
|
79
|
+
if (middleware_result !== undefined) {
|
|
80
|
+
return /** @type {ResultValue} */ (middleware_result);
|
|
81
|
+
}
|
|
82
|
+
if (next_promise !== null) {
|
|
83
|
+
return next_promise;
|
|
84
|
+
}
|
|
85
|
+
return await next_handler();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} pathname
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
export function is_hashed_asset(pathname) {
|
|
93
|
+
return pathname.includes('.') && /[a-f0-9]{8,}/.test(pathname);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} pathname
|
|
98
|
+
* @param {number} [max_age]
|
|
99
|
+
* @param {boolean} [immutable]
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
export function get_static_cache_control(
|
|
103
|
+
pathname,
|
|
104
|
+
max_age = DEFAULT_STATIC_MAX_AGE,
|
|
105
|
+
immutable = false,
|
|
106
|
+
) {
|
|
107
|
+
if (immutable || is_hashed_asset(pathname)) {
|
|
108
|
+
return 'public, max-age=31536000, immutable';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return `public, max-age=${max_age}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} pathname
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
export function get_mime_type(pathname) {
|
|
119
|
+
const extension = extname(pathname).toLowerCase();
|
|
120
|
+
return MIME_TYPES[extension] || 'application/octet-stream';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} base_dir
|
|
125
|
+
* @param {string} pathname
|
|
126
|
+
* @returns {string | null}
|
|
127
|
+
*/
|
|
128
|
+
function resolve_static_file_path(base_dir, pathname) {
|
|
129
|
+
const file_path = resolve(base_dir, `.${pathname}`);
|
|
130
|
+
const is_within_base_dir = file_path === base_dir || file_path.startsWith(base_dir + sep);
|
|
131
|
+
return is_within_base_dir ? file_path : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a request-based static file handler that can be used by adapters.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} dir - Directory to serve files from (relative to cwd or absolute)
|
|
138
|
+
* @param {{ prefix?: string, maxAge?: number, immutable?: boolean }} [options]
|
|
139
|
+
* @returns {(request: Request) => Response | null}
|
|
140
|
+
*/
|
|
141
|
+
export function serveStatic(dir, options = {}) {
|
|
142
|
+
const {
|
|
143
|
+
prefix = DEFAULT_STATIC_PREFIX,
|
|
144
|
+
maxAge = DEFAULT_STATIC_MAX_AGE,
|
|
145
|
+
immutable = false,
|
|
146
|
+
} = options;
|
|
147
|
+
|
|
148
|
+
const base_dir = resolve(dir);
|
|
149
|
+
|
|
150
|
+
return function serve_static_request(request) {
|
|
151
|
+
const request_method = (request.method || 'GET').toUpperCase();
|
|
152
|
+
if (request_method !== 'GET' && request_method !== 'HEAD') {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let pathname;
|
|
157
|
+
try {
|
|
158
|
+
pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname);
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!pathname.startsWith(prefix)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pathname = pathname.slice(prefix.length) || '/';
|
|
168
|
+
if (!pathname.startsWith('/')) {
|
|
169
|
+
pathname = '/' + pathname;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const file_path = resolve_static_file_path(base_dir, pathname);
|
|
173
|
+
if (file_path === null || !existsSync(file_path)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let file_stats;
|
|
178
|
+
try {
|
|
179
|
+
file_stats = statSync(file_path);
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (file_stats.isDirectory()) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const headers = new Headers();
|
|
189
|
+
headers.set('Content-Type', get_mime_type(file_path));
|
|
190
|
+
headers.set('Content-Length', String(file_stats.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 });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// `Readable.toWeb()` can be typed differently across Node/Bun/libdom contexts.
|
|
198
|
+
/** @type {BodyInit} */
|
|
199
|
+
const file_body = /** @type {any} */ (Readable.toWeb(createReadStream(file_path)));
|
|
200
|
+
return new Response(file_body, { status: 200, headers });
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_HOSTNAME,
|
|
7
|
+
DEFAULT_PORT,
|
|
8
|
+
get_mime_type,
|
|
9
|
+
get_static_cache_control,
|
|
10
|
+
internal_server_error_response,
|
|
11
|
+
is_hashed_asset,
|
|
12
|
+
run_next_middleware,
|
|
13
|
+
serveStatic,
|
|
14
|
+
} from '../src/index.js';
|
|
15
|
+
|
|
16
|
+
describe('@ripple-ts/adapter', () => {
|
|
17
|
+
it('exports stable default host and port', () => {
|
|
18
|
+
expect(DEFAULT_PORT).toBe(3000);
|
|
19
|
+
expect(DEFAULT_HOSTNAME).toBe('localhost');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('creates standardized internal server error response', async () => {
|
|
23
|
+
const response = internal_server_error_response();
|
|
24
|
+
expect(response.status).toBe(500);
|
|
25
|
+
expect(response.headers.get('content-type')).toBe('text/plain; charset=utf-8');
|
|
26
|
+
expect(await response.text()).toBe('Internal Server Error');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('run_next_middleware returns middleware response when short-circuited', async () => {
|
|
30
|
+
const response = await run_next_middleware(
|
|
31
|
+
() => new Response('middleware'),
|
|
32
|
+
new Request('http://localhost/'),
|
|
33
|
+
{ id: 'server' },
|
|
34
|
+
async () => new Response('handler'),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(await response.text()).toBe('middleware');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('run_next_middleware resolves next() only once and returns handler response', async () => {
|
|
41
|
+
let handler_calls = 0;
|
|
42
|
+
|
|
43
|
+
const response = await run_next_middleware(
|
|
44
|
+
async (request, server, next) => {
|
|
45
|
+
void request;
|
|
46
|
+
void server;
|
|
47
|
+
await next();
|
|
48
|
+
return await next();
|
|
49
|
+
},
|
|
50
|
+
new Request('http://localhost/'),
|
|
51
|
+
{ id: 'server' },
|
|
52
|
+
async () => {
|
|
53
|
+
handler_calls += 1;
|
|
54
|
+
return new Response('handler');
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(handler_calls).toBe(1);
|
|
59
|
+
expect(await response.text()).toBe('handler');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('run_next_middleware falls through to next handler when middleware returns void', async () => {
|
|
63
|
+
const response = await run_next_middleware(
|
|
64
|
+
() => {},
|
|
65
|
+
new Request('http://localhost/'),
|
|
66
|
+
{ id: 'server' },
|
|
67
|
+
async () => new Response('handler'),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(await response.text()).toBe('handler');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('run_next_middleware supports non-Response result types', async () => {
|
|
74
|
+
const value = await run_next_middleware(
|
|
75
|
+
() => 'middleware-value',
|
|
76
|
+
{ request: 1 },
|
|
77
|
+
{ id: 'server' },
|
|
78
|
+
async () => 'handler-value',
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(value).toBe('middleware-value');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('resolves MIME types with fallback', () => {
|
|
85
|
+
expect(get_mime_type('app.js')).toBe('text/javascript; charset=utf-8');
|
|
86
|
+
expect(get_mime_type('image.svg')).toBe('image/svg+xml');
|
|
87
|
+
expect(get_mime_type('file.unknown')).toBe('application/octet-stream');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('detects hashed assets and computes cache-control', () => {
|
|
91
|
+
expect(is_hashed_asset('/assets/app.2f1abce9.js')).toBe(true);
|
|
92
|
+
expect(is_hashed_asset('/assets/app.js')).toBe(false);
|
|
93
|
+
expect(get_static_cache_control('/assets/app.2f1abce9.js')).toBe(
|
|
94
|
+
'public, max-age=31536000, immutable',
|
|
95
|
+
);
|
|
96
|
+
expect(get_static_cache_control('/assets/app.js', 60, false)).toBe('public, max-age=60');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('serveStatic serves files for GET and HEAD requests', async () => {
|
|
100
|
+
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-static-'));
|
|
101
|
+
try {
|
|
102
|
+
writeFileSync(join(temp_dir, 'app.js'), 'console.log("ok");');
|
|
103
|
+
|
|
104
|
+
const static_handler = serveStatic(temp_dir, { prefix: '/assets' });
|
|
105
|
+
|
|
106
|
+
const get_response = static_handler(new Request('http://localhost/assets/app.js'));
|
|
107
|
+
expect(get_response).not.toBeNull();
|
|
108
|
+
if (get_response === null) {
|
|
109
|
+
throw new Error('Expected static GET response');
|
|
110
|
+
}
|
|
111
|
+
expect(get_response.status).toBe(200);
|
|
112
|
+
expect(get_response.headers.get('content-type')).toBe('text/javascript; charset=utf-8');
|
|
113
|
+
expect(get_response.headers.get('cache-control')).toBe('public, max-age=86400');
|
|
114
|
+
expect(await get_response.text()).toContain('console.log');
|
|
115
|
+
|
|
116
|
+
const head_response = static_handler(
|
|
117
|
+
new Request('http://localhost/assets/app.js', { method: 'HEAD' }),
|
|
118
|
+
);
|
|
119
|
+
expect(head_response).not.toBeNull();
|
|
120
|
+
if (head_response === null) {
|
|
121
|
+
throw new Error('Expected static HEAD response');
|
|
122
|
+
}
|
|
123
|
+
expect(head_response.status).toBe(200);
|
|
124
|
+
expect(await head_response.text()).toBe('');
|
|
125
|
+
} finally {
|
|
126
|
+
rmSync(temp_dir, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('serveStatic falls through for non-matching requests', () => {
|
|
131
|
+
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-static-fallthrough-'));
|
|
132
|
+
try {
|
|
133
|
+
writeFileSync(join(temp_dir, 'index.html'), '<h1>ok</h1>');
|
|
134
|
+
const static_handler = serveStatic(temp_dir, { prefix: '/assets' });
|
|
135
|
+
|
|
136
|
+
expect(static_handler(new Request('http://localhost/index.html'))).toBeNull();
|
|
137
|
+
expect(static_handler(new Request('http://localhost/assets/missing.js'))).toBeNull();
|
|
138
|
+
expect(
|
|
139
|
+
static_handler(new Request('http://localhost/assets/index.html', { method: 'POST' })),
|
|
140
|
+
).toBeNull();
|
|
141
|
+
} finally {
|
|
142
|
+
rmSync(temp_dir, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('serveStatic applies configured cache-control options', async () => {
|
|
147
|
+
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-static-cache-'));
|
|
148
|
+
try {
|
|
149
|
+
writeFileSync(join(temp_dir, 'app.js'), 'console.log("cache");');
|
|
150
|
+
|
|
151
|
+
const max_age_handler = serveStatic(temp_dir, { prefix: '/assets', maxAge: 120 });
|
|
152
|
+
const max_age_response = max_age_handler(new Request('http://localhost/assets/app.js'));
|
|
153
|
+
expect(max_age_response).not.toBeNull();
|
|
154
|
+
if (max_age_response === null) {
|
|
155
|
+
throw new Error('Expected static response');
|
|
156
|
+
}
|
|
157
|
+
expect(max_age_response.headers.get('cache-control')).toBe('public, max-age=120');
|
|
158
|
+
|
|
159
|
+
const immutable_handler = serveStatic(temp_dir, {
|
|
160
|
+
prefix: '/assets',
|
|
161
|
+
maxAge: 120,
|
|
162
|
+
immutable: true,
|
|
163
|
+
});
|
|
164
|
+
const immutable_response = immutable_handler(new Request('http://localhost/assets/app.js'));
|
|
165
|
+
expect(immutable_response).not.toBeNull();
|
|
166
|
+
if (immutable_response === null) {
|
|
167
|
+
throw new Error('Expected static response');
|
|
168
|
+
}
|
|
169
|
+
expect(immutable_response.headers.get('cache-control')).toBe(
|
|
170
|
+
'public, max-age=31536000, immutable',
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
rmSync(temp_dir, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('serveStatic blocks traversal and directory targets', () => {
|
|
178
|
+
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-static-security-'));
|
|
179
|
+
try {
|
|
180
|
+
const public_dir = join(temp_dir, 'public');
|
|
181
|
+
mkdirSync(public_dir);
|
|
182
|
+
mkdirSync(join(public_dir, 'nested'));
|
|
183
|
+
writeFileSync(join(temp_dir, 'secret.txt'), 'secret');
|
|
184
|
+
|
|
185
|
+
const static_handler = serveStatic(public_dir, { prefix: '/assets' });
|
|
186
|
+
expect(static_handler(new Request('http://localhost/assets/%2e%2e/secret.txt'))).toBeNull();
|
|
187
|
+
expect(static_handler(new Request('http://localhost/assets/nested'))).toBeNull();
|
|
188
|
+
} finally {
|
|
189
|
+
rmSync(temp_dir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -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 types', () => {
|
|
7
|
+
it('exports ServeStaticOptions with cache-control fields', () => {
|
|
8
|
+
expect(types_source).toMatch(
|
|
9
|
+
/export type ServeStaticOptions = \{[\s\S]*?prefix\?: string;[\s\S]*?maxAge\?: number;[\s\S]*?immutable\?: boolean;[\s\S]*?\};/,
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('exports ServeStaticDirectoryOptions with shared dir field', () => {
|
|
14
|
+
expect(types_source).toMatch(
|
|
15
|
+
/export type ServeStaticDirectoryOptions = ServeStaticOptions & \{[\s\S]*?dir\?: string;[\s\S]*?\};/,
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
});
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type FetchHandler<Platform = any, ResultValue = Response> = (
|
|
2
|
+
request: Request,
|
|
3
|
+
platform?: Platform,
|
|
4
|
+
) => ResultValue | Promise<ResultValue>;
|
|
5
|
+
|
|
6
|
+
export type AdapterCoreOptions = {
|
|
7
|
+
port?: number;
|
|
8
|
+
hostname?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ServeStaticOptions = {
|
|
12
|
+
prefix?: string;
|
|
13
|
+
maxAge?: number;
|
|
14
|
+
immutable?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ServeStaticDirectoryOptions = ServeStaticOptions & {
|
|
18
|
+
dir?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type NextMiddleware<RequestValue = Request, Server = any, ResultValue = Response> = (
|
|
22
|
+
request: RequestValue,
|
|
23
|
+
server: Server,
|
|
24
|
+
next: () => Promise<ResultValue>,
|
|
25
|
+
) => ResultValue | Promise<ResultValue> | void;
|
|
26
|
+
|
|
27
|
+
export type ServeResult<Server = any> = {
|
|
28
|
+
listen: (port?: number) => Server;
|
|
29
|
+
close: () => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_HOSTNAME: 'localhost';
|
|
33
|
+
export const DEFAULT_PORT: 3000;
|
|
34
|
+
export const DEFAULT_STATIC_PREFIX: '/';
|
|
35
|
+
export const DEFAULT_STATIC_MAX_AGE: 86400;
|
|
36
|
+
export const MIME_TYPES: Readonly<Record<string, string>>;
|
|
37
|
+
|
|
38
|
+
export function internal_server_error_response(): Response;
|
|
39
|
+
|
|
40
|
+
export function run_next_middleware<RequestValue, Server, ResultValue = Response>(
|
|
41
|
+
middleware: NextMiddleware<RequestValue, Server, ResultValue>,
|
|
42
|
+
request: RequestValue,
|
|
43
|
+
server: Server,
|
|
44
|
+
next_handler: () => Promise<ResultValue>,
|
|
45
|
+
): Promise<ResultValue>;
|
|
46
|
+
|
|
47
|
+
export function is_hashed_asset(pathname: string): boolean;
|
|
48
|
+
|
|
49
|
+
export function get_static_cache_control(
|
|
50
|
+
pathname: string,
|
|
51
|
+
max_age?: number,
|
|
52
|
+
immutable?: boolean,
|
|
53
|
+
): string;
|
|
54
|
+
|
|
55
|
+
export function get_mime_type(pathname: string): string;
|
|
56
|
+
|
|
57
|
+
export function serveStatic(
|
|
58
|
+
dir: string,
|
|
59
|
+
options?: ServeStaticOptions,
|
|
60
|
+
): (request: Request) => Response | null;
|