@oselvar/sveltekit-add-worker-exports 0.3.3 → 1.0.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/README.md +141 -82
- package/dist/index.js +59 -49
- package/package.json +1 -5
package/README.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
# @oselvar/sveltekit-add-worker-exports
|
|
2
2
|
|
|
3
|
-
A Vite plugin that makes Durable Objects
|
|
3
|
+
A Vite plugin that makes any class-based Cloudflare Worker export (Durable Objects, Workflows, `WorkerEntrypoint` RPC, voice agents, …) work with SvelteKit on Cloudflare, in both dev and production.
|
|
4
4
|
|
|
5
|
-
**Build mode:** SvelteKit's `adapter-cloudflare` generates `_worker.js` with only a default export (the fetch handler). Cloudflare Workers requires Durable
|
|
5
|
+
**Build mode:** SvelteKit's `adapter-cloudflare` generates `_worker.js` with only a default export (the fetch handler). Cloudflare Workers requires class-based bindings (Durable Objects, Workflows, `WorkerEntrypoint`, etc.) to be **named exports**. This plugin post-processes the build output to merge your named exports with SvelteKit's default export.
|
|
6
6
|
|
|
7
|
-
**Dev mode:** `getPlatformProxy` (used by `adapter-cloudflare` in dev) can't run internal Durable Objects or
|
|
7
|
+
**Dev mode:** `getPlatformProxy` (used by `adapter-cloudflare` in dev) can't run internal Durable Objects, Workflows, or other class-based bindings. This plugin starts a separate wrangler dev server that runs the real worker with hot-reload. SvelteKit `+server.ts` handlers call bindings through `platform.env.MY_BINDING.<rpc>()` as usual — the plugin rewrites those bindings to point at the sidecar via wrangler's dev registry, so cross-worker calls Just Work. WebSocket clients in dev connect to the sidecar directly on a separate port — see [WebSockets](#websockets) below.
|
|
8
|
+
|
|
9
|
+
## What this works with
|
|
10
|
+
|
|
11
|
+
The plugin is binding-agnostic: if Cloudflare resolves it through a named class export plus a `wrangler.jsonc` binding, the plugin handles it. Confirmed working:
|
|
12
|
+
|
|
13
|
+
- Durable Objects
|
|
14
|
+
- Workflows (`WorkflowEntrypoint`)
|
|
15
|
+
- Voice agents
|
|
16
|
+
|
|
17
|
+
It should also work with `WorkerEntrypoint` RPC and any future class-based export that follows the same export-plus-binding pattern. The examples below use Durable Objects and Workflows because they're the most common, but the wiring is the same for any class-based entrypoint — export the class from your entry point, declare the binding in `wrangler.jsonc`, and call it via `platform.env.MY_BINDING` in your routes.
|
|
8
18
|
|
|
9
19
|
## Install
|
|
10
20
|
|
|
@@ -16,61 +26,17 @@ pnpm add -D @oselvar/sveltekit-add-worker-exports
|
|
|
16
26
|
|
|
17
27
|
## Usage
|
|
18
28
|
|
|
19
|
-
Create a worker entry point that exports your
|
|
29
|
+
Create a worker entry point that exports your class-based bindings:
|
|
20
30
|
|
|
21
31
|
```typescript
|
|
22
32
|
// src/lib/server/index.ts
|
|
23
33
|
export { MyDurableObject } from './MyDurableObject';
|
|
24
34
|
export { MyWorkflow } from './MyWorkflow';
|
|
25
|
-
export { default } from './devHandler';
|
|
26
35
|
```
|
|
27
36
|
|
|
28
37
|
Workflow classes (extending `WorkflowEntrypoint`) are exported the same way as Durable Objects — the plugin merges them into `_worker.js` as named exports. Declare them in `wrangler.jsonc` under `workflows`, and they become available as bindings (e.g. `env.MY_WORKFLOW.create({ params })`) in both dev and production.
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// src/lib/server/forwardWebSocket.ts
|
|
34
|
-
export async function forwardWebSocket<T extends Rpc.DurableObjectBranded | undefined>(
|
|
35
|
-
request: Request,
|
|
36
|
-
namespace: DurableObjectNamespace<T>,
|
|
37
|
-
name: string
|
|
38
|
-
): Promise<Response> {
|
|
39
|
-
if (request.headers.get('upgrade') !== 'websocket') {
|
|
40
|
-
return new Response('Expected WebSocket', { status: 426 });
|
|
41
|
-
}
|
|
42
|
-
const id = namespace.idFromName(name);
|
|
43
|
-
return namespace.get(id).fetch(request);
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
The dev handler parses the URL and delegates. It's only used by the wrangler dev sidecar:
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
// src/lib/server/devHandler.ts
|
|
51
|
-
import { forwardWebSocket } from './forwardWebSocket';
|
|
52
|
-
|
|
53
|
-
export default {
|
|
54
|
-
async fetch(request: Request, env: Env): Promise<Response> {
|
|
55
|
-
const match = new URL(request.url).pathname.match(/^\/ws\/(.+)$/);
|
|
56
|
-
if (!match) return new Response('Not found', { status: 404 });
|
|
57
|
-
return forwardWebSocket(request, env.MY_DO, match[1]);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
In production, the dev handler is *not* used — SvelteKit serves the same path through a `+server.ts` route, which delegates to the same helper:
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
// src/routes/ws/[id]/+server.ts
|
|
66
|
-
import { forwardWebSocket } from '$lib/server/forwardWebSocket';
|
|
67
|
-
import type { RequestHandler } from './$types';
|
|
68
|
-
|
|
69
|
-
export const GET: RequestHandler = ({ params, request, platform }) =>
|
|
70
|
-
forwardWebSocket(request, platform!.env.MY_DO, params.id);
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Now any change to the upgrade-and-forward logic (auth, rate-limiting, response shape) lives in one place and applies to both dev and production.
|
|
39
|
+
If clients need to talk directly to the dev sidecar over HTTP (e.g. WebSockets), also export a default fetch handler — see [WebSockets](#websockets) below.
|
|
74
40
|
|
|
75
41
|
Add the plugin to your `vite.config.ts` **after** `sveltekit()`:
|
|
76
42
|
|
|
@@ -104,36 +70,11 @@ export default {
|
|
|
104
70
|
};
|
|
105
71
|
```
|
|
106
72
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
In dev mode, the plugin starts a wrangler dev server on a separate port and injects `__DEV_WORKER_PORT__` as a compile-time constant. Use it to connect your client:
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
import { dev } from '$app/environment';
|
|
73
|
+
The plugin auto-discovers your `wrangler.jsonc` (or `wrangler.toml`) and reads bindings, workflows, migrations, and compatibility settings from it. It overrides only the `main` entry point to point at your source entry.
|
|
113
74
|
|
|
114
|
-
|
|
115
|
-
if (dev) {
|
|
116
|
-
wsUrl = `ws://${window.location.hostname}:${__DEV_WORKER_PORT__}/ws/${id}`;
|
|
117
|
-
} else {
|
|
118
|
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
119
|
-
wsUrl = `${protocol}//${window.location.host}/ws/${id}`;
|
|
120
|
-
}
|
|
121
|
-
const ws = new WebSocket(wsUrl);
|
|
122
|
-
```
|
|
75
|
+
### Calling Workflows
|
|
123
76
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
declare global {
|
|
128
|
-
const __DEV_WORKER_PORT__: number;
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
The plugin auto-discovers your `wrangler.jsonc` (or `wrangler.toml`) and reads DO bindings, workflows, migrations, and compatibility settings from it. It overrides only the `main` entry point to point at your source entry.
|
|
133
|
-
|
|
134
|
-
### Dev mode: calling Workflows
|
|
135
|
-
|
|
136
|
-
`+server.ts` calls workflows the same way it does in production:
|
|
77
|
+
`+server.ts` calls workflows the same way in dev and production:
|
|
137
78
|
|
|
138
79
|
```typescript
|
|
139
80
|
export const POST: RequestHandler = async ({ params, request, platform }) => {
|
|
@@ -149,14 +90,14 @@ The sidecar runs the real `WorkflowEntrypoint` class; calls reach it via the `sc
|
|
|
149
90
|
|
|
150
91
|
### Testing the production build locally
|
|
151
92
|
|
|
152
|
-
`vite dev` exercises
|
|
93
|
+
`vite dev` exercises your code via the wrangler-dev sidecar; it does *not* exercise the merged `_worker.js`. To verify the production wiring (named class exports + SvelteKit routes in the same worker), run wrangler against the build output:
|
|
153
94
|
|
|
154
95
|
```bash
|
|
155
96
|
pnpm build # produces .svelte-kit/cloudflare/_worker.js with merged exports
|
|
156
97
|
pnpm wrangler dev # uses wrangler.jsonc → main: .svelte-kit/cloudflare/_worker.js
|
|
157
98
|
```
|
|
158
99
|
|
|
159
|
-
This serves the exact bundle that gets deployed, with local Durable Object storage.
|
|
100
|
+
This serves the exact bundle that gets deployed, with local Durable Object storage. Exercise whichever route calls your binding (HTTP, WebSocket, whatever your app uses) and confirm the response — that proves the request flowed through the SvelteKit `+server.ts` and into your class.
|
|
160
101
|
|
|
161
102
|
A handy shortcut is to add a `preview` script to `package.json`:
|
|
162
103
|
|
|
@@ -170,6 +111,109 @@ A handy shortcut is to add a `preview` script to `package.json`:
|
|
|
170
111
|
|
|
171
112
|
> Note: `vite preview` is *not* suitable here — it only serves static assets and cannot run Durable Objects. Always use `wrangler dev` to preview the production worker.
|
|
172
113
|
|
|
114
|
+
## Scheduled, queue, email, tail handlers
|
|
115
|
+
|
|
116
|
+
Cloudflare invokes non-fetch handlers (`scheduled`, `queue`, `email`, `tail`, `trace`) as methods on the worker's default export, not as named exports. Put them on the `default` export of your entry point and the plugin merges them onto the production worker's default alongside SvelteKit's fetch handler:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// src/lib/server/index.ts
|
|
120
|
+
export { MyDurableObject } from './MyDurableObject';
|
|
121
|
+
export default {
|
|
122
|
+
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
|
|
123
|
+
// runs from the cron triggers in wrangler.jsonc
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If you also need a dev-only `fetch` (e.g. for WebSocket upgrades), put it on the same default — the plugin keeps the non-`fetch` handlers and drops `fetch` in production. See [WebSockets](#websockets) below for the full shape.
|
|
129
|
+
|
|
130
|
+
### Firing scheduled in dev
|
|
131
|
+
|
|
132
|
+
Wrangler dev never auto-fires crons — `triggers.crons` in `wrangler.jsonc` is only honored on the real Cloudflare edge. The plugin enables `testScheduled` on the sidecar so you can invoke the handler manually:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
curl 'http://localhost:8787/__scheduled?cron=*+*+*+*+*'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The `cron` query parameter (URL-encoded — `+` instead of spaces) is what gets passed to your handler as `event.cron`. Stdout from the sidecar lands in the vite terminal.
|
|
139
|
+
|
|
140
|
+
## WebSockets
|
|
141
|
+
|
|
142
|
+
`vite dev` doesn't proxy WebSocket upgrades to the wrangler-dev sidecar, so in dev mode the browser needs to connect to the sidecar directly. The plugin exposes the sidecar's port as a compile-time constant `__DEV_WORKER_PORT__` for exactly this.
|
|
143
|
+
|
|
144
|
+
The pattern: one helper handles the upgrade-and-forward, called from both the dev sidecar's fetch handler and the production `+server.ts` route, so both code paths stay in sync.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// src/lib/server/forwardWebSocket.ts
|
|
148
|
+
export async function forwardWebSocket<T extends Rpc.DurableObjectBranded | undefined>(
|
|
149
|
+
request: Request,
|
|
150
|
+
namespace: DurableObjectNamespace<T>,
|
|
151
|
+
name: string
|
|
152
|
+
): Promise<Response> {
|
|
153
|
+
if (request.headers.get('upgrade') !== 'websocket') {
|
|
154
|
+
return new Response('Expected WebSocket', { status: 426 });
|
|
155
|
+
}
|
|
156
|
+
const id = namespace.idFromName(name);
|
|
157
|
+
return namespace.get(id).fetch(request);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Put a `fetch` on the entry's default export so the dev sidecar can receive the direct connection:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// src/lib/server/index.ts
|
|
165
|
+
import { forwardWebSocket } from './forwardWebSocket';
|
|
166
|
+
|
|
167
|
+
export { MyDurableObject } from './MyDurableObject';
|
|
168
|
+
export { MyWorkflow } from './MyWorkflow';
|
|
169
|
+
|
|
170
|
+
export default {
|
|
171
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
172
|
+
const match = new URL(request.url).pathname.match(/^\/ws\/(.+)$/);
|
|
173
|
+
if (!match) return new Response('Not found', { status: 404 });
|
|
174
|
+
return forwardWebSocket(request, env.MY_DO, match[1]);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
In production this `fetch` is dropped (SvelteKit owns request handling — the plugin's merge strips it). The same path is served through a `+server.ts` route that delegates to the same helper:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// src/routes/ws/[id]/+server.ts
|
|
183
|
+
import { forwardWebSocket } from '$lib/server/forwardWebSocket';
|
|
184
|
+
import type { RequestHandler } from './$types';
|
|
185
|
+
|
|
186
|
+
export const GET: RequestHandler = ({ params, request, platform }) =>
|
|
187
|
+
forwardWebSocket(request, platform!.env.MY_DO, params.id);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Any change to the upgrade-and-forward logic (auth, rate-limiting, response shape) now lives in one place and applies to both dev and production.
|
|
191
|
+
|
|
192
|
+
### Connecting from the client
|
|
193
|
+
|
|
194
|
+
Use `__DEV_WORKER_PORT__` to pick between the sidecar (dev) and the SvelteKit route (production):
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { dev } from '$app/environment';
|
|
198
|
+
|
|
199
|
+
let wsUrl: string;
|
|
200
|
+
if (dev) {
|
|
201
|
+
wsUrl = `ws://${window.location.hostname}:${__DEV_WORKER_PORT__}/ws/${id}`;
|
|
202
|
+
} else {
|
|
203
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
204
|
+
wsUrl = `${protocol}//${window.location.host}/ws/${id}`;
|
|
205
|
+
}
|
|
206
|
+
const ws = new WebSocket(wsUrl);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Add the type declaration to your `src/app.d.ts`:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
declare global {
|
|
213
|
+
const __DEV_WORKER_PORT__: number;
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
173
217
|
## Options
|
|
174
218
|
|
|
175
219
|
| Option | Type | Required | Default | Description |
|
|
@@ -187,11 +231,14 @@ The plugin runs in the `closeBundle` hook (after SvelteKit's adapter has generat
|
|
|
187
231
|
|
|
188
232
|
1. Bundles your `entryPoint` with esbuild into `_extra_exports.js`
|
|
189
233
|
2. Renames the original `_worker.js` to `_sveltekit_worker.js`
|
|
190
|
-
3. Creates a new `_worker.js` that re-exports
|
|
234
|
+
3. Creates a new `_worker.js` that re-exports the named exports and merges the entry's `default` handlers (`scheduled`, `queue`, `email`, …) onto the SvelteKit default:
|
|
191
235
|
|
|
192
236
|
```javascript
|
|
193
|
-
|
|
237
|
+
import sveltekitWorker from './_sveltekit_worker.js';
|
|
238
|
+
import * as extra from './_extra_exports.js';
|
|
194
239
|
export * from './_extra_exports.js';
|
|
240
|
+
const { fetch: _ignored, ...extraHandlers } = extra.default ?? {};
|
|
241
|
+
export default { ...sveltekitWorker, ...extraHandlers };
|
|
195
242
|
```
|
|
196
243
|
|
|
197
244
|
The operation is idempotent -- if `_sveltekit_worker.js` already exists, the plugin skips.
|
|
@@ -229,7 +276,19 @@ Note: the `.types-worker-wrangler.jsonc` file is generated when the dev server s
|
|
|
229
276
|
|
|
230
277
|
## Why this exists
|
|
231
278
|
|
|
232
|
-
SvelteKit's adapter-cloudflare does not support named exports from the worker entry point ([sveltejs/kit#1712](https://github.com/sveltejs/kit/issues/1712)). Additionally, `getPlatformProxy` (used for local dev) cannot run internal
|
|
279
|
+
SvelteKit's adapter-cloudflare does not support named exports from the worker entry point ([sveltejs/kit#1712](https://github.com/sveltejs/kit/issues/1712)) — which blocks every class-based Cloudflare binding, not just Durable Objects. Additionally, `getPlatformProxy` (used for local dev) cannot run internal class-based bindings because it uses an empty worker script.
|
|
280
|
+
|
|
281
|
+
## When this plugin can be retired
|
|
282
|
+
|
|
283
|
+
This plugin is a stopgap. The single blocker for retirement is [sveltejs/kit#15627](https://github.com/sveltejs/kit/pull/15627) — which replaces `adapter-cloudflare`'s custom build with `@cloudflare/vite-plugin`. Open as of writing. Closes [#1712](https://github.com/sveltejs/kit/issues/1712), [#10496](https://github.com/sveltejs/kit/issues/10496), [#13692](https://github.com/sveltejs/kit/issues/13692), [#2963](https://github.com/sveltejs/kit/issues/2963), [#13300](https://github.com/sveltejs/kit/issues/13300), [#1519](https://github.com/sveltejs/kit/issues/1519).
|
|
284
|
+
|
|
285
|
+
Once that PR lands, `@cloudflare/vite-plugin` handles everything this plugin does, natively:
|
|
286
|
+
|
|
287
|
+
- **Build:** the plugin bundles a single worker entry that exports `default` + named classes directly — no `_worker.js` post-processing, no second esbuild pass to merge exports.
|
|
288
|
+
- **Dev:** Durable Objects and Workflows run in the same worker as SvelteKit on real workerd, hot-reloaded — no sidecar via `unstable_startWorker`, no platform-proxy `script_name` rewrite, no separate WebSocket port, no dev registry routing.
|
|
289
|
+
- **Types:** `wrangler types` resolves typed bindings (`DurableObjectNamespace<MyDO>`) from the user entry, no `.types-worker-wrangler.jsonc` indirection.
|
|
290
|
+
|
|
291
|
+
Side note: [cloudflare/workers-sdk#14013](https://github.com/cloudflare/workers-sdk/pull/14013) introduces an experimental `cloudflare.config.ts` flow (typed `exports` API) where the Durable Object branch currently throws `"Durable Object exports are not currently supported."` (see [`convert.ts`](https://github.com/cloudflare/workers-sdk/blob/main/packages/config/src/convert.ts)) and Workflows are commented out. This is a separate, opt-in config surface — the standard `wrangler.jsonc` path that kit#15627 uses already supports DO and Workflow named exports, so this limitation does **not** gate retirement.
|
|
233
292
|
|
|
234
293
|
## License
|
|
235
294
|
|
package/dist/index.js
CHANGED
|
@@ -23,20 +23,12 @@
|
|
|
23
23
|
import { build } from 'esbuild';
|
|
24
24
|
import { existsSync } from 'node:fs';
|
|
25
25
|
import { access, readFile, rename, writeFile, unlink } from 'node:fs/promises';
|
|
26
|
+
import { builtinModules as NODE_BUILTINS } from 'node:module';
|
|
26
27
|
import { homedir } from 'node:os';
|
|
27
28
|
import { join, resolve } from 'node:path';
|
|
28
29
|
import { parse as parseJsonc } from 'jsonc-parser';
|
|
30
|
+
import { getNodeCompat } from 'miniflare';
|
|
29
31
|
const DEFAULT_DEV_PORT = 8787;
|
|
30
|
-
const NODE_BUILTINS = [
|
|
31
|
-
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console',
|
|
32
|
-
'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain',
|
|
33
|
-
'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector',
|
|
34
|
-
'module', 'net', 'os', 'path', 'path/posix', 'path/win32', 'perf_hooks',
|
|
35
|
-
'process', 'punycode', 'querystring', 'readline', 'repl', 'stream',
|
|
36
|
-
'stream/consumers', 'stream/promises', 'stream/web', 'string_decoder',
|
|
37
|
-
'sys', 'timers', 'timers/promises', 'tls', 'trace_events', 'tty', 'url',
|
|
38
|
-
'util', 'util/types', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib'
|
|
39
|
-
];
|
|
40
32
|
/**
|
|
41
33
|
* Mirrors wrangler's internal getRegistryPath() — wrangler doesn't export it.
|
|
42
34
|
* Workers register themselves under this path so cross-worker `script_name`
|
|
@@ -117,9 +109,22 @@ function buildPlugin(options) {
|
|
|
117
109
|
],
|
|
118
110
|
outfile: exportsPath
|
|
119
111
|
});
|
|
120
|
-
// Rename original worker and create merged entry point
|
|
112
|
+
// Rename original worker and create merged entry point.
|
|
113
|
+
//
|
|
114
|
+
// `export *` re-exports named exports only, so class-based bindings
|
|
115
|
+
// (DOs, Workflows, WorkerEntrypoint) from `_extra_exports.js` flow
|
|
116
|
+
// through. Non-fetch handlers (scheduled, queue, email, tail, trace)
|
|
117
|
+
// live on the user entry's `default` export — Cloudflare invokes
|
|
118
|
+
// them as methods on the worker's default object, not as named
|
|
119
|
+
// exports — so we spread them onto the SvelteKit default. `fetch`
|
|
120
|
+
// is dropped from the user default because SvelteKit owns request
|
|
121
|
+
// handling in production.
|
|
121
122
|
await rename(workerPath, sveltekitPath);
|
|
122
|
-
await writeFile(workerPath, `
|
|
123
|
+
await writeFile(workerPath, `import sveltekitWorker from './_sveltekit_worker.js';\n` +
|
|
124
|
+
`import * as extra from './_extra_exports.js';\n` +
|
|
125
|
+
`export * from './_extra_exports.js';\n` +
|
|
126
|
+
`const { fetch: _ignored, ...extraHandlers } = extra.default ?? {};\n` +
|
|
127
|
+
`export default { ...sveltekitWorker, ...extraHandlers };\n`);
|
|
123
128
|
}
|
|
124
129
|
};
|
|
125
130
|
}
|
|
@@ -147,15 +152,30 @@ function devPlugin(options) {
|
|
|
147
152
|
async configureServer(server) {
|
|
148
153
|
const { unstable_startWorker } = await import('wrangler');
|
|
149
154
|
const rawJson = await readRawConfig();
|
|
150
|
-
const
|
|
155
|
+
const baseConfig = parseJsonc(rawJson);
|
|
156
|
+
const sidecarName = `${baseConfig.name ?? 'sveltekit'}-dev-worker`;
|
|
157
|
+
// When CLOUDFLARE_ENV is set, wrangler suffixes the registered
|
|
158
|
+
// worker name with `-${env}` (see appendEnvName in wrangler), so
|
|
159
|
+
// cross-worker `script_name` lookups must use the suffixed name.
|
|
160
|
+
// We don't suffix `devConfig.name` itself — wrangler does that
|
|
161
|
+
// based on CLOUDFLARE_ENV when starting the sidecar.
|
|
162
|
+
const envName = process.env.CLOUDFLARE_ENV;
|
|
163
|
+
const registeredSidecarName = envName ? `${sidecarName}-${envName}` : sidecarName;
|
|
151
164
|
// Copy the raw config and override main, name, and dev port
|
|
152
|
-
const devConfig =
|
|
165
|
+
const devConfig = structuredClone(baseConfig);
|
|
153
166
|
devConfig.name = sidecarName;
|
|
154
167
|
devConfig.main = options.entryPoint;
|
|
155
168
|
devConfig.dev = { ...devConfig.dev, port: devPort };
|
|
156
169
|
delete devConfig.assets;
|
|
170
|
+
// Serialised dev config is reused verbatim as the `wrangler types`
|
|
171
|
+
// config — the user can run `wrangler types --config
|
|
172
|
+
// .types-worker-wrangler.jsonc` to get typed bindings like
|
|
173
|
+
// DurableObjectNamespace<EchoDO> resolved from the source entry.
|
|
174
|
+
const devConfigJson = JSON.stringify(devConfig, null, '\t');
|
|
157
175
|
tempConfigPath = resolve('.dev-worker-wrangler.jsonc');
|
|
158
|
-
|
|
176
|
+
typesConfigPath = resolve('.types-worker-wrangler.jsonc');
|
|
177
|
+
await writeFile(tempConfigPath, devConfigJson);
|
|
178
|
+
await writeFile(typesConfigPath, devConfigJson);
|
|
159
179
|
// Write a wrangler config for adapter-cloudflare's getPlatformProxy
|
|
160
180
|
// (used by vite dev for platform.env). It can't run internal DOs or
|
|
161
181
|
// Workflows itself -- those are served by the sidecar above. Rewrite
|
|
@@ -163,30 +183,32 @@ function devPlugin(options) {
|
|
|
163
183
|
// (script_name pointing at the sidecar) so platform.env.MY_DO and
|
|
164
184
|
// platform.env.MY_WORKFLOW calls in +server.ts reach the sidecar via
|
|
165
185
|
// the wrangler dev registry.
|
|
166
|
-
const proxyConfig =
|
|
167
|
-
|
|
168
|
-
|
|
186
|
+
const proxyConfig = structuredClone(baseConfig);
|
|
187
|
+
// Wrangler does not merge per-env `durable_objects`/`workflows`/
|
|
188
|
+
// `migrations` arrays with the top-level — the selected env wholly
|
|
189
|
+
// overrides them. So we patch only the scope wrangler will actually
|
|
190
|
+
// use: top-level when no env, env[envName] when CLOUDFLARE_ENV is set.
|
|
191
|
+
const activeScope = envName ? proxyConfig.env?.[envName] : proxyConfig;
|
|
192
|
+
if (envName && !activeScope) {
|
|
193
|
+
throw new Error(`CLOUDFLARE_ENV="${envName}" but no \`env.${envName}\` block in the wrangler config`);
|
|
169
194
|
}
|
|
170
|
-
if (
|
|
171
|
-
|
|
195
|
+
if (activeScope.durable_objects?.bindings) {
|
|
196
|
+
activeScope.durable_objects.bindings = activeScope.durable_objects.bindings.map((b) => b.script_name ? b : { ...b, script_name: registeredSidecarName });
|
|
172
197
|
}
|
|
173
|
-
|
|
198
|
+
if (Array.isArray(activeScope.workflows)) {
|
|
199
|
+
activeScope.workflows = activeScope.workflows.map((w) => w.script_name ? w : { ...w, script_name: registeredSidecarName });
|
|
200
|
+
}
|
|
201
|
+
delete activeScope.migrations;
|
|
174
202
|
proxyConfigPath = resolve('.platform-proxy-wrangler.jsonc');
|
|
175
203
|
await writeFile(proxyConfigPath, JSON.stringify(proxyConfig, null, '\t'));
|
|
176
|
-
// A third config used only by `wrangler types`. Identical to the dev
|
|
177
|
-
// config but kept separate so the user can run `wrangler types
|
|
178
|
-
// --config .types-worker-wrangler.jsonc` to get typed bindings like
|
|
179
|
-
// DurableObjectNamespace<EchoDO> resolved from the source entry.
|
|
180
|
-
const typesConfig = parseJsonc(rawJson);
|
|
181
|
-
typesConfig.name = sidecarName;
|
|
182
|
-
typesConfig.main = options.entryPoint;
|
|
183
|
-
typesConfig.dev = { ...typesConfig.dev, port: devPort };
|
|
184
|
-
delete typesConfig.assets;
|
|
185
|
-
typesConfigPath = resolve('.types-worker-wrangler.jsonc');
|
|
186
|
-
await writeFile(typesConfigPath, JSON.stringify(typesConfig, null, '\t'));
|
|
187
204
|
worker = await unstable_startWorker({
|
|
188
205
|
config: tempConfigPath,
|
|
189
|
-
|
|
206
|
+
// `testScheduled` mounts a `/__scheduled` endpoint on the sidecar
|
|
207
|
+
// so cron handlers can be invoked manually in dev — wrangler dev
|
|
208
|
+
// never auto-fires crons. Curl
|
|
209
|
+
// `http://localhost:<devPort>/__scheduled?cron=*+*+*+*+*` to
|
|
210
|
+
// trigger your `scheduled` handler.
|
|
211
|
+
dev: { registry: getWranglerRegistryPath(), testScheduled: true },
|
|
190
212
|
build: {
|
|
191
213
|
nodejsCompatMode: (parsedConfig) => getNodejsCompatMode(parsedConfig.compatibility_date, parsedConfig.compatibility_flags)
|
|
192
214
|
}
|
|
@@ -219,23 +241,11 @@ async function exists(path) {
|
|
|
219
241
|
}
|
|
220
242
|
}
|
|
221
243
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* compat plugin and warns about `node:*` imports even when
|
|
244
|
+
* wrangler's `unstable_startWorker` does not derive this from the config
|
|
245
|
+
* flags automatically — without it, wrangler falls back to the legacy
|
|
246
|
+
* nodejs compat plugin and warns about `node:*` imports even when
|
|
226
247
|
* `nodejs_compat` is enabled.
|
|
227
248
|
*/
|
|
228
249
|
function getNodejsCompatMode(compatibilityDate, compatibilityFlags = []) {
|
|
229
|
-
|
|
230
|
-
const hasCompatV2 = compatibilityFlags.includes('nodejs_compat_v2');
|
|
231
|
-
const hasNoCompatV2 = compatibilityFlags.includes('no_nodejs_compat_v2');
|
|
232
|
-
const hasAls = compatibilityFlags.includes('nodejs_als');
|
|
233
|
-
const date = compatibilityDate ?? '2000-01-01';
|
|
234
|
-
if (hasCompatV2 || (hasCompat && date >= '2024-09-23' && !hasNoCompatV2))
|
|
235
|
-
return 'v2';
|
|
236
|
-
if (hasCompat)
|
|
237
|
-
return 'v1';
|
|
238
|
-
if (hasAls)
|
|
239
|
-
return 'als';
|
|
240
|
-
return null;
|
|
250
|
+
return getNodeCompat(compatibilityDate, [...compatibilityFlags]).mode;
|
|
241
251
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oselvar/sveltekit-add-worker-exports",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Vite plugin that adds Durable Object and Workflow exports to SvelteKit Cloudflare workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
|
-
},
|
|
14
|
-
"./hooks": {
|
|
15
|
-
"types": "./dist/hooks.d.ts",
|
|
16
|
-
"import": "./dist/hooks.js"
|
|
17
13
|
}
|
|
18
14
|
},
|
|
19
15
|
"files": [
|