@vertz/cloudflare 0.2.0
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/.turbo/turbo-build.log +15 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +24 -0
- package/package.json +40 -0
- package/src/handler.ts +28 -0
- package/src/index.ts +2 -0
- package/tests/handler.test.ts +178 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
$ bunup
|
|
2
|
+
[34mi[39m [2mUsing bunup v0.16.26 and bun v1.3.9[22m
|
|
3
|
+
[34mi[39m Build started
|
|
4
|
+
|
|
5
|
+
[2msrc/index.ts[22m
|
|
6
|
+
|
|
7
|
+
[2m Output Raw Gzip[22m
|
|
8
|
+
|
|
9
|
+
[2mdist/[22mindex.js 676 B 349 B
|
|
10
|
+
[2mdist/[22m[32m[1mindex.d.ts[22m[39m 327 B 225 B
|
|
11
|
+
|
|
12
|
+
[1m2 files [22m [1m1003 B[22m [1m574 B[22m
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[32m✓[39m Build completed in [32m[32m114ms[32m[39m
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AppBuilder } from "@vertz/core";
|
|
2
|
+
interface CloudflareHandlerOptions {
|
|
3
|
+
basePath?: string;
|
|
4
|
+
}
|
|
5
|
+
declare function createHandler(app: AppBuilder, options?: CloudflareHandlerOptions): {
|
|
6
|
+
fetch(request: Request, _env: unknown, _ctx: ExecutionContext): Promise<Response>;
|
|
7
|
+
};
|
|
8
|
+
export { createHandler, CloudflareHandlerOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/handler.ts
|
|
2
|
+
function createHandler(app, options) {
|
|
3
|
+
const handler = app.handler;
|
|
4
|
+
return {
|
|
5
|
+
async fetch(request, _env, _ctx) {
|
|
6
|
+
if (options?.basePath) {
|
|
7
|
+
const url = new URL(request.url);
|
|
8
|
+
if (url.pathname.startsWith(options.basePath)) {
|
|
9
|
+
url.pathname = url.pathname.slice(options.basePath.length) || "/";
|
|
10
|
+
request = new Request(url.toString(), request);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return await handler(request);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error("Unhandled error in worker:", error);
|
|
17
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
createHandler
|
|
24
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vertz/cloudflare",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Cloudflare Workers adapter for vertz",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/vertz-dev/vertz.git",
|
|
11
|
+
"directory": "packages/cloudflare"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "bunup",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@vertz/core": "workspace:*"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@cloudflare/workers-types": "^4.20250214.0",
|
|
29
|
+
"bunup": "latest",
|
|
30
|
+
"typescript": "^5.7.3"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@vertz/ui-server": "workspace:*"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"@vertz/ui-server": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/handler.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AppBuilder } from '@vertz/core';
|
|
2
|
+
|
|
3
|
+
export interface CloudflareHandlerOptions {
|
|
4
|
+
basePath?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createHandler(app: AppBuilder, options?: CloudflareHandlerOptions) {
|
|
8
|
+
const handler = app.handler;
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
async fetch(request: Request, _env: unknown, _ctx: ExecutionContext): Promise<Response> {
|
|
12
|
+
// If basePath, strip it from the URL before routing
|
|
13
|
+
if (options?.basePath) {
|
|
14
|
+
const url = new URL(request.url);
|
|
15
|
+
if (url.pathname.startsWith(options.basePath)) {
|
|
16
|
+
url.pathname = url.pathname.slice(options.basePath.length) || '/';
|
|
17
|
+
request = new Request(url.toString(), request);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return await handler(request);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Unhandled error in worker:', error);
|
|
24
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { AppBuilder } from '@vertz/core';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { createHandler } from '../src/handler.js';
|
|
4
|
+
|
|
5
|
+
describe('createHandler', () => {
|
|
6
|
+
it('returns proper Worker export with fetch method', () => {
|
|
7
|
+
const mockHandler = vi.fn().mockResolvedValue(new Response('OK'));
|
|
8
|
+
const mockApp = {
|
|
9
|
+
handler: mockHandler,
|
|
10
|
+
} as unknown as AppBuilder;
|
|
11
|
+
|
|
12
|
+
const worker = createHandler(mockApp);
|
|
13
|
+
|
|
14
|
+
expect(worker).toHaveProperty('fetch');
|
|
15
|
+
expect(typeof worker.fetch).toBe('function');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('forwards requests to the vertz handler', async () => {
|
|
19
|
+
const mockResponse = new Response('Hello from handler');
|
|
20
|
+
const mockHandler = vi.fn().mockResolvedValue(mockResponse);
|
|
21
|
+
const mockApp = {
|
|
22
|
+
handler: mockHandler,
|
|
23
|
+
} as unknown as AppBuilder;
|
|
24
|
+
|
|
25
|
+
const worker = createHandler(mockApp);
|
|
26
|
+
const request = new Request('https://example.com/api/test');
|
|
27
|
+
const mockEnv = {};
|
|
28
|
+
const mockCtx = {} as ExecutionContext;
|
|
29
|
+
|
|
30
|
+
const response = await worker.fetch(request, mockEnv, mockCtx);
|
|
31
|
+
|
|
32
|
+
expect(mockHandler).toHaveBeenCalledWith(request);
|
|
33
|
+
expect(response).toBe(mockResponse);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('strips basePath prefix from pathname', async () => {
|
|
37
|
+
const mockHandler = vi.fn().mockResolvedValue(new Response('OK'));
|
|
38
|
+
const mockApp = {
|
|
39
|
+
handler: mockHandler,
|
|
40
|
+
} as unknown as AppBuilder;
|
|
41
|
+
|
|
42
|
+
const worker = createHandler(mockApp, { basePath: '/api' });
|
|
43
|
+
const request = new Request('https://example.com/api/users');
|
|
44
|
+
const mockEnv = {};
|
|
45
|
+
const mockCtx = {} as ExecutionContext;
|
|
46
|
+
|
|
47
|
+
await worker.fetch(request, mockEnv, mockCtx);
|
|
48
|
+
|
|
49
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
50
|
+
const calledRequest = mockHandler.mock.calls[0][0] as Request;
|
|
51
|
+
const url = new URL(calledRequest.url);
|
|
52
|
+
expect(url.pathname).toBe('/users');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('strips basePath with trailing slash correctly', async () => {
|
|
56
|
+
const mockHandler = vi.fn().mockResolvedValue(new Response('OK'));
|
|
57
|
+
const mockApp = {
|
|
58
|
+
handler: mockHandler,
|
|
59
|
+
} as unknown as AppBuilder;
|
|
60
|
+
|
|
61
|
+
const worker = createHandler(mockApp, { basePath: '/api' });
|
|
62
|
+
const request = new Request('https://example.com/api/');
|
|
63
|
+
const mockEnv = {};
|
|
64
|
+
const mockCtx = {} as ExecutionContext;
|
|
65
|
+
|
|
66
|
+
await worker.fetch(request, mockEnv, mockCtx);
|
|
67
|
+
|
|
68
|
+
const calledRequest = mockHandler.mock.calls[0][0] as Request;
|
|
69
|
+
const url = new URL(calledRequest.url);
|
|
70
|
+
expect(url.pathname).toBe('/');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles basePath when pathname does not start with basePath', async () => {
|
|
74
|
+
const mockHandler = vi.fn().mockResolvedValue(new Response('OK'));
|
|
75
|
+
const mockApp = {
|
|
76
|
+
handler: mockHandler,
|
|
77
|
+
} as unknown as AppBuilder;
|
|
78
|
+
|
|
79
|
+
const worker = createHandler(mockApp, { basePath: '/api' });
|
|
80
|
+
const request = new Request('https://example.com/other/path');
|
|
81
|
+
const mockEnv = {};
|
|
82
|
+
const mockCtx = {} as ExecutionContext;
|
|
83
|
+
|
|
84
|
+
await worker.fetch(request, mockEnv, mockCtx);
|
|
85
|
+
|
|
86
|
+
const calledRequest = mockHandler.mock.calls[0][0] as Request;
|
|
87
|
+
const url = new URL(calledRequest.url);
|
|
88
|
+
expect(url.pathname).toBe('/other/path');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('preserves query parameters when stripping basePath', async () => {
|
|
92
|
+
const mockHandler = vi.fn().mockResolvedValue(new Response('OK'));
|
|
93
|
+
const mockApp = {
|
|
94
|
+
handler: mockHandler,
|
|
95
|
+
} as unknown as AppBuilder;
|
|
96
|
+
|
|
97
|
+
const worker = createHandler(mockApp, { basePath: '/api' });
|
|
98
|
+
const request = new Request('https://example.com/api/users?page=1&limit=10');
|
|
99
|
+
const mockEnv = {};
|
|
100
|
+
const mockCtx = {} as ExecutionContext;
|
|
101
|
+
|
|
102
|
+
await worker.fetch(request, mockEnv, mockCtx);
|
|
103
|
+
|
|
104
|
+
const calledRequest = mockHandler.mock.calls[0][0] as Request;
|
|
105
|
+
const url = new URL(calledRequest.url);
|
|
106
|
+
expect(url.pathname).toBe('/users');
|
|
107
|
+
expect(url.searchParams.get('page')).toBe('1');
|
|
108
|
+
expect(url.searchParams.get('limit')).toBe('10');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('preserves request headers and method', async () => {
|
|
112
|
+
const mockHandler = vi.fn().mockResolvedValue(new Response('OK'));
|
|
113
|
+
const mockApp = {
|
|
114
|
+
handler: mockHandler,
|
|
115
|
+
} as unknown as AppBuilder;
|
|
116
|
+
|
|
117
|
+
const worker = createHandler(mockApp, { basePath: '/api' });
|
|
118
|
+
const request = new Request('https://example.com/api/users', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: {
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
Authorization: 'Bearer token123',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const mockEnv = {};
|
|
126
|
+
const mockCtx = {} as ExecutionContext;
|
|
127
|
+
|
|
128
|
+
await worker.fetch(request, mockEnv, mockCtx);
|
|
129
|
+
|
|
130
|
+
const calledRequest = mockHandler.mock.calls[0][0] as Request;
|
|
131
|
+
expect(calledRequest.method).toBe('POST');
|
|
132
|
+
expect(calledRequest.headers.get('Content-Type')).toBe('application/json');
|
|
133
|
+
expect(calledRequest.headers.get('Authorization')).toBe('Bearer token123');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('works without basePath option', async () => {
|
|
137
|
+
const mockResponse = new Response('No basePath');
|
|
138
|
+
const mockHandler = vi.fn().mockResolvedValue(mockResponse);
|
|
139
|
+
const mockApp = {
|
|
140
|
+
handler: mockHandler,
|
|
141
|
+
} as unknown as AppBuilder;
|
|
142
|
+
|
|
143
|
+
const worker = createHandler(mockApp);
|
|
144
|
+
const request = new Request('https://example.com/api/test');
|
|
145
|
+
const mockEnv = {};
|
|
146
|
+
const mockCtx = {} as ExecutionContext;
|
|
147
|
+
|
|
148
|
+
const response = await worker.fetch(request, mockEnv, mockCtx);
|
|
149
|
+
|
|
150
|
+
expect(mockHandler).toHaveBeenCalledWith(request);
|
|
151
|
+
expect(response).toBe(mockResponse);
|
|
152
|
+
const calledRequest = mockHandler.mock.calls[0][0] as Request;
|
|
153
|
+
expect(new URL(calledRequest.url).pathname).toBe('/api/test');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('returns 500 response when handler throws an error', async () => {
|
|
157
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
158
|
+
const testError = new Error('Test error');
|
|
159
|
+
const mockHandler = vi.fn().mockRejectedValue(testError);
|
|
160
|
+
const mockApp = {
|
|
161
|
+
handler: mockHandler,
|
|
162
|
+
} as unknown as AppBuilder;
|
|
163
|
+
|
|
164
|
+
const worker = createHandler(mockApp);
|
|
165
|
+
const request = new Request('https://example.com/api/test');
|
|
166
|
+
const mockEnv = {};
|
|
167
|
+
const mockCtx = {} as ExecutionContext;
|
|
168
|
+
|
|
169
|
+
const response = await worker.fetch(request, mockEnv, mockCtx);
|
|
170
|
+
|
|
171
|
+
expect(mockHandler).toHaveBeenCalledWith(request);
|
|
172
|
+
expect(response.status).toBe(500);
|
|
173
|
+
expect(await response.text()).toBe('Internal Server Error');
|
|
174
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Unhandled error in worker:', testError);
|
|
175
|
+
|
|
176
|
+
consoleErrorSpy.mockRestore();
|
|
177
|
+
});
|
|
178
|
+
});
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { defineConfig } from 'vitest/config';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
test: {
|
|
9
|
+
include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
|
|
10
|
+
environment: 'node',
|
|
11
|
+
alias: {
|
|
12
|
+
'@': resolve(__dirname, './src'),
|
|
13
|
+
},
|
|
14
|
+
coverage: {
|
|
15
|
+
reporter: ['text', 'json-summary', 'json'],
|
|
16
|
+
provider: 'v8',
|
|
17
|
+
include: ['src/**/*.ts'],
|
|
18
|
+
exclude: ['src/**/*.test.ts', 'src/index.ts'],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|