@velajs/cloudflare 1.1.0 → 1.3.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.
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import { type VelaApplication } from '@velajs/vela';
|
|
3
3
|
import type { CloudflareEnv } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Options accepted by {@link CloudflareApplication.mountOpenApi}.
|
|
6
|
+
*
|
|
7
|
+
* Re-exposes vela's `MountOpenApiOptions` type — derived structurally from
|
|
8
|
+
* the underlying `VelaApplication.mountOpenApi` signature so consumers don't
|
|
9
|
+
* have to reach into vela's internal subpaths to type the argument.
|
|
10
|
+
*/
|
|
11
|
+
export type MountOpenApiOptions = Parameters<VelaApplication['mountOpenApi']>[0];
|
|
4
12
|
/**
|
|
5
13
|
* Wraps VelaApplication with Cloudflare-specific handlers:
|
|
6
14
|
* - `fetch` — HTTP request handler (from Hono)
|
|
7
15
|
* - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators
|
|
8
16
|
* AND vela's own `@Cron()` jobs)
|
|
9
17
|
* - `queue` — Queue consumer handler (matches `@QueueConsumer()` decorators)
|
|
18
|
+
* - `mountOpenApi` — Serve an OpenAPI document (and optional Scalar UI) on
|
|
19
|
+
* the underlying Hono app
|
|
10
20
|
*
|
|
11
21
|
* @example
|
|
12
22
|
* ```ts
|
|
@@ -17,6 +27,16 @@ import type { CloudflareEnv } from './types';
|
|
|
17
27
|
* queue: app.queue.bind(app),
|
|
18
28
|
* };
|
|
19
29
|
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* // Serve OpenAPI docs alongside your routes
|
|
34
|
+
* const app = await createCloudflareApp(AppModule);
|
|
35
|
+
* const document = createOpenApiDocument(AppModule);
|
|
36
|
+
* app.mountOpenApi({ document, ui: 'scalar' });
|
|
37
|
+
* // GET /docs.json -> JSON document
|
|
38
|
+
* // GET /docs -> Scalar UI (loads from CDN)
|
|
39
|
+
* ```
|
|
20
40
|
*/
|
|
21
41
|
export declare class CloudflareApplication {
|
|
22
42
|
private app;
|
|
@@ -25,6 +45,27 @@ export declare class CloudflareApplication {
|
|
|
25
45
|
constructor(app: VelaApplication);
|
|
26
46
|
get fetch(): Hono['fetch'];
|
|
27
47
|
getHonoApp(): Hono;
|
|
48
|
+
/**
|
|
49
|
+
* Serve a pre-built OpenAPI document (and optionally a Scalar UI) on the
|
|
50
|
+
* underlying Hono app. Delegates verbatim to `VelaApplication.mountOpenApi`,
|
|
51
|
+
* so the JSON endpoint defaults to `/docs.json` and the Scalar UI (when
|
|
52
|
+
* opted in) defaults to `/docs`. Edge-safe — the UI HTML loads Scalar from
|
|
53
|
+
* a CDN at runtime, nothing is bundled server-side.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { createOpenApiDocument } from '@velajs/vela';
|
|
58
|
+
*
|
|
59
|
+
* const app = await createCloudflareApp(AppModule);
|
|
60
|
+
* const document = createOpenApiDocument(AppModule, {
|
|
61
|
+
* info: { title: 'My API', version: '1.0.0' },
|
|
62
|
+
* });
|
|
63
|
+
* app.mountOpenApi({ document, ui: 'scalar' });
|
|
64
|
+
* // GET /docs.json -> { openapi: '3.1.0', ... }
|
|
65
|
+
* // GET /docs -> Scalar UI HTML
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
mountOpenApi(options: MountOpenApiOptions): this;
|
|
28
69
|
/** @internal — scans instances for @Scheduled, @Cron, and @QueueConsumer metadata */
|
|
29
70
|
scanInstances(instances: unknown[]): void;
|
|
30
71
|
/**
|
|
@@ -14,6 +14,8 @@ function invoke(instance, methodName, args) {
|
|
|
14
14
|
* - `scheduled` — Cron trigger handler (matches `@Scheduled()` decorators
|
|
15
15
|
* AND vela's own `@Cron()` jobs)
|
|
16
16
|
* - `queue` — Queue consumer handler (matches `@QueueConsumer()` decorators)
|
|
17
|
+
* - `mountOpenApi` — Serve an OpenAPI document (and optional Scalar UI) on
|
|
18
|
+
* the underlying Hono app
|
|
17
19
|
*
|
|
18
20
|
* @example
|
|
19
21
|
* ```ts
|
|
@@ -24,6 +26,16 @@ function invoke(instance, methodName, args) {
|
|
|
24
26
|
* queue: app.queue.bind(app),
|
|
25
27
|
* };
|
|
26
28
|
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Serve OpenAPI docs alongside your routes
|
|
33
|
+
* const app = await createCloudflareApp(AppModule);
|
|
34
|
+
* const document = createOpenApiDocument(AppModule);
|
|
35
|
+
* app.mountOpenApi({ document, ui: 'scalar' });
|
|
36
|
+
* // GET /docs.json -> JSON document
|
|
37
|
+
* // GET /docs -> Scalar UI (loads from CDN)
|
|
38
|
+
* ```
|
|
27
39
|
*/ export class CloudflareApplication {
|
|
28
40
|
app;
|
|
29
41
|
scheduledHandlers = [];
|
|
@@ -37,6 +49,29 @@ function invoke(instance, methodName, args) {
|
|
|
37
49
|
getHonoApp() {
|
|
38
50
|
return this.app.getHonoApp();
|
|
39
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Serve a pre-built OpenAPI document (and optionally a Scalar UI) on the
|
|
54
|
+
* underlying Hono app. Delegates verbatim to `VelaApplication.mountOpenApi`,
|
|
55
|
+
* so the JSON endpoint defaults to `/docs.json` and the Scalar UI (when
|
|
56
|
+
* opted in) defaults to `/docs`. Edge-safe — the UI HTML loads Scalar from
|
|
57
|
+
* a CDN at runtime, nothing is bundled server-side.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { createOpenApiDocument } from '@velajs/vela';
|
|
62
|
+
*
|
|
63
|
+
* const app = await createCloudflareApp(AppModule);
|
|
64
|
+
* const document = createOpenApiDocument(AppModule, {
|
|
65
|
+
* info: { title: 'My API', version: '1.0.0' },
|
|
66
|
+
* });
|
|
67
|
+
* app.mountOpenApi({ document, ui: 'scalar' });
|
|
68
|
+
* // GET /docs.json -> { openapi: '3.1.0', ... }
|
|
69
|
+
* // GET /docs -> Scalar UI HTML
|
|
70
|
+
* ```
|
|
71
|
+
*/ mountOpenApi(options) {
|
|
72
|
+
this.app.mountOpenApi(options);
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
40
75
|
/** @internal — scans instances for @Scheduled, @Cron, and @QueueConsumer metadata */ scanInstances(instances) {
|
|
41
76
|
for (const instance of instances){
|
|
42
77
|
if (!instance || typeof instance !== 'object') continue;
|
|
@@ -1,15 +1,74 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
1
2
|
import type { Type } from '@velajs/vela';
|
|
2
3
|
import { CloudflareApplication } from './cloudflare-application';
|
|
4
|
+
/**
|
|
5
|
+
* Options for {@link createCloudflareApp}.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors a tight subset of vela's `BootstrapOptions` — only the surface
|
|
8
|
+
* that makes sense for a Workers consumer is re-exposed.
|
|
9
|
+
*/
|
|
10
|
+
export interface CreateCloudflareAppOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Forwarded to `VelaFactory.create({ globalPrefix })`. Prepended to every
|
|
13
|
+
* route registered by `@Controller(...)` (and any other route emitter)
|
|
14
|
+
* inside the application, so a value of `'/v1'` turns `@Controller('/users')`
|
|
15
|
+
* into `/v1/users`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const app = await createCloudflareApp(AppModule, { globalPrefix: '/v1' });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
globalPrefix?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Extra Hono middleware to register on the underlying Hono app. Runs
|
|
25
|
+
* AFTER the one-time binding-init middleware this adapter mounts
|
|
26
|
+
* internally, so any handler in `middleware` can safely read from
|
|
27
|
+
* `BindingRef` instances and Cloudflare env bindings.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const app = await createCloudflareApp(AppModule, {
|
|
32
|
+
* middleware: [
|
|
33
|
+
* async (c, next) => {
|
|
34
|
+
* c.set('requestId', crypto.randomUUID());
|
|
35
|
+
* await next();
|
|
36
|
+
* },
|
|
37
|
+
* ],
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
middleware?: MiddlewareHandler[];
|
|
42
|
+
}
|
|
3
43
|
/**
|
|
4
44
|
* Create a Cloudflare Workers application.
|
|
5
45
|
*
|
|
6
46
|
* Sets up a one-time Hono middleware that captures `c.env` on the first
|
|
7
|
-
* request and initializes all configured binding refs.
|
|
47
|
+
* request and initializes all configured binding refs. Optional
|
|
48
|
+
* {@link CreateCloudflareAppOptions} are forwarded to the underlying
|
|
49
|
+
* `VelaFactory.create` so consumers don't have to wrap the resulting
|
|
50
|
+
* application in an outer Hono just to set a `globalPrefix` or attach
|
|
51
|
+
* extra request middleware.
|
|
8
52
|
*
|
|
9
53
|
* @example
|
|
10
54
|
* ```ts
|
|
55
|
+
* // Minimal — backwards compatible
|
|
11
56
|
* const app = await createCloudflareApp(AppModule);
|
|
12
57
|
* export default app; // has .fetch, .scheduled, .queue
|
|
13
58
|
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* // With a global prefix and outer middleware
|
|
63
|
+
* const app = await createCloudflareApp(AppModule, {
|
|
64
|
+
* globalPrefix: '/v1',
|
|
65
|
+
* middleware: [
|
|
66
|
+
* async (c, next) => {
|
|
67
|
+
* c.set('tenantId', c.req.header('x-tenant-id') ?? 'public');
|
|
68
|
+
* await next();
|
|
69
|
+
* },
|
|
70
|
+
* ],
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
14
73
|
*/
|
|
15
|
-
export declare function createCloudflareApp(rootModule: Type): Promise<CloudflareApplication>;
|
|
74
|
+
export declare function createCloudflareApp(rootModule: Type, options?: CreateCloudflareAppOptions): Promise<CloudflareApplication>;
|
|
@@ -22,28 +22,52 @@ function collectBindingRefs(container) {
|
|
|
22
22
|
* Create a Cloudflare Workers application.
|
|
23
23
|
*
|
|
24
24
|
* Sets up a one-time Hono middleware that captures `c.env` on the first
|
|
25
|
-
* request and initializes all configured binding refs.
|
|
25
|
+
* request and initializes all configured binding refs. Optional
|
|
26
|
+
* {@link CreateCloudflareAppOptions} are forwarded to the underlying
|
|
27
|
+
* `VelaFactory.create` so consumers don't have to wrap the resulting
|
|
28
|
+
* application in an outer Hono just to set a `globalPrefix` or attach
|
|
29
|
+
* extra request middleware.
|
|
26
30
|
*
|
|
27
31
|
* @example
|
|
28
32
|
* ```ts
|
|
33
|
+
* // Minimal — backwards compatible
|
|
29
34
|
* const app = await createCloudflareApp(AppModule);
|
|
30
35
|
* export default app; // has .fetch, .scheduled, .queue
|
|
31
36
|
* ```
|
|
32
|
-
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* // With a global prefix and outer middleware
|
|
41
|
+
* const app = await createCloudflareApp(AppModule, {
|
|
42
|
+
* globalPrefix: '/v1',
|
|
43
|
+
* middleware: [
|
|
44
|
+
* async (c, next) => {
|
|
45
|
+
* c.set('tenantId', c.req.header('x-tenant-id') ?? 'public');
|
|
46
|
+
* await next();
|
|
47
|
+
* },
|
|
48
|
+
* ],
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*/ export async function createCloudflareApp(rootModule, options = {}) {
|
|
33
52
|
let initialized = false;
|
|
34
53
|
let refs;
|
|
54
|
+
const bindingInit = async (c, next)=>{
|
|
55
|
+
if (!initialized) {
|
|
56
|
+
initialized = true;
|
|
57
|
+
const env = c.env ?? {};
|
|
58
|
+
for (const ref of refs){
|
|
59
|
+
ref._initialize(env[ref.bindingName]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await next();
|
|
63
|
+
};
|
|
64
|
+
// IMPORTANT: keep the binding-init middleware FIRST so user middleware
|
|
65
|
+
// can safely read binding refs / `c.env` derivatives on the first request.
|
|
35
66
|
const velaApp = await VelaFactory.create(rootModule, {
|
|
67
|
+
globalPrefix: options.globalPrefix,
|
|
36
68
|
middleware: [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
initialized = true;
|
|
40
|
-
const env = c.env ?? {};
|
|
41
|
-
for (const ref of refs){
|
|
42
|
-
ref._initialize(env[ref.bindingName]);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
await next();
|
|
46
|
-
}
|
|
69
|
+
bindingInit,
|
|
70
|
+
...options.middleware ?? []
|
|
47
71
|
]
|
|
48
72
|
});
|
|
49
73
|
refs = collectBindingRefs(velaApp.getContainer());
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { createCloudflareApp } from './cloudflare-factory';
|
|
2
|
+
export type { CreateCloudflareAppOptions } from './cloudflare-factory';
|
|
2
3
|
export { CloudflareApplication } from './cloudflare-application';
|
|
4
|
+
export type { MountOpenApiOptions } from './cloudflare-application';
|
|
3
5
|
export { KVModule } from './modules/kv.module';
|
|
4
6
|
export { D1Module } from './modules/d1.module';
|
|
5
7
|
export { R2Module } from './modules/r2.module';
|
|
@@ -1,14 +1,33 @@
|
|
|
1
|
-
import { createModuleRef } from "@velajs/vela";
|
|
2
1
|
import { BindingRef } from "../binding-ref.js";
|
|
3
2
|
// Single factory for every Cloudflare binding module (KV, D1, R2, ...).
|
|
4
3
|
// Each binding module is just a bag of (token, service); this generates
|
|
5
4
|
// the canonical forRoot() shape for them.
|
|
5
|
+
//
|
|
6
|
+
// Identity model (post vela audit #2):
|
|
7
|
+
// - One real module class is declared per binding TYPE (KV, D1, R2, ...).
|
|
8
|
+
// Its `name` property is set so error messages and diagnostics carry
|
|
9
|
+
// the friendly name (e.g. `KVModule`).
|
|
10
|
+
// - Each `forRoot({ binding })` returns `{ module, key: binding, ... }`.
|
|
11
|
+
// Vela dedups by (module, key), so two `KVModule.forRoot({...})` calls
|
|
12
|
+
// with different bindings coexist as distinct module instances; two
|
|
13
|
+
// calls with the same binding dedup.
|
|
6
14
|
export function createBindingModule(opts) {
|
|
15
|
+
// Fresh module class per binding TYPE (one KVModule, one D1Module, ...).
|
|
16
|
+
// The computed-property-name idiom is the canonical JS way to give a
|
|
17
|
+
// class expression a dynamic `name`: NamedEvaluation reads the property
|
|
18
|
+
// key and stamps it on the class at creation, instead of patching the
|
|
19
|
+
// (configurable, non-writable) `name` slot via Object.defineProperty.
|
|
20
|
+
const className = `${opts.name}Module`;
|
|
21
|
+
const moduleClass = {
|
|
22
|
+
[className]: class {
|
|
23
|
+
}
|
|
24
|
+
}[className];
|
|
7
25
|
return {
|
|
8
26
|
forRoot ({ binding }) {
|
|
9
27
|
const ref = new BindingRef(binding);
|
|
10
28
|
return {
|
|
11
|
-
module:
|
|
29
|
+
module: moduleClass,
|
|
30
|
+
key: binding,
|
|
12
31
|
providers: [
|
|
13
32
|
{
|
|
14
33
|
provide: opts.bindingRefToken,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/cloudflare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Cloudflare Workers integration for Vela framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,13 +17,6 @@
|
|
|
17
17
|
"LICENSE",
|
|
18
18
|
"CHANGELOG.md"
|
|
19
19
|
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
|
|
22
|
-
"test": "vitest run",
|
|
23
|
-
"typecheck": "tsc --noEmit",
|
|
24
|
-
"prepare": "swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
|
|
25
|
-
"prepublishOnly": "pnpm run typecheck && pnpm test"
|
|
26
|
-
},
|
|
27
20
|
"sideEffects": false,
|
|
28
21
|
"keywords": [
|
|
29
22
|
"vela",
|
|
@@ -52,24 +45,22 @@
|
|
|
52
45
|
},
|
|
53
46
|
"peerDependencies": {
|
|
54
47
|
"@cloudflare/workers-types": ">=4",
|
|
55
|
-
"@velajs/vela": ">=1.
|
|
48
|
+
"@velajs/vela": ">=1.5.0",
|
|
56
49
|
"hono": ">=4"
|
|
57
50
|
},
|
|
58
51
|
"devDependencies": {
|
|
59
52
|
"@cloudflare/workers-types": "^4",
|
|
60
53
|
"@swc/cli": "^0.8.0",
|
|
61
54
|
"@swc/core": "^1.15.11",
|
|
62
|
-
"@velajs/vela": "^1.
|
|
55
|
+
"@velajs/vela": "^1.5.0",
|
|
63
56
|
"hono": "^4",
|
|
64
57
|
"typescript": "^5",
|
|
65
58
|
"unplugin-swc": "^1.5.9",
|
|
66
59
|
"vitest": "^4.0.18"
|
|
67
60
|
},
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
"esbuild"
|
|
73
|
-
]
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"typecheck": "tsc --noEmit"
|
|
74
65
|
}
|
|
75
|
-
}
|
|
66
|
+
}
|