@oselvar/sveltekit-add-worker-exports 0.3.4 → 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.
Files changed (3) hide show
  1. package/README.md +49 -14
  2. package/dist/index.js +59 -49
  3. package/package.json +1 -5
package/README.md CHANGED
@@ -111,6 +111,32 @@ A handy shortcut is to add a `preview` script to `package.json`:
111
111
 
112
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.
113
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
+
114
140
  ## WebSockets
115
141
 
116
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.
@@ -132,12 +158,15 @@ export async function forwardWebSocket<T extends Rpc.DurableObjectBranded | unde
132
158
  }
133
159
  ```
134
160
 
135
- The dev sidecar needs a default `fetch` handler to receive the direct connection:
161
+ Put a `fetch` on the entry's default export so the dev sidecar can receive the direct connection:
136
162
 
137
163
  ```typescript
138
- // src/lib/server/devHandler.ts
164
+ // src/lib/server/index.ts
139
165
  import { forwardWebSocket } from './forwardWebSocket';
140
166
 
167
+ export { MyDurableObject } from './MyDurableObject';
168
+ export { MyWorkflow } from './MyWorkflow';
169
+
141
170
  export default {
142
171
  async fetch(request: Request, env: Env): Promise<Response> {
143
172
  const match = new URL(request.url).pathname.match(/^\/ws\/(.+)$/);
@@ -147,16 +176,7 @@ export default {
147
176
  };
148
177
  ```
149
178
 
150
- Add it to the worker entry point as the default export:
151
-
152
- ```typescript
153
- // src/lib/server/index.ts
154
- export { MyDurableObject } from './MyDurableObject';
155
- export { MyWorkflow } from './MyWorkflow';
156
- export { default } from './devHandler';
157
- ```
158
-
159
- In production, the dev handler is *not* used — SvelteKit serves the same path through a `+server.ts` route, which delegates to the same helper:
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:
160
180
 
161
181
  ```typescript
162
182
  // src/routes/ws/[id]/+server.ts
@@ -211,11 +231,14 @@ The plugin runs in the `closeBundle` hook (after SvelteKit's adapter has generat
211
231
 
212
232
  1. Bundles your `entryPoint` with esbuild into `_extra_exports.js`
213
233
  2. Renames the original `_worker.js` to `_sveltekit_worker.js`
214
- 3. Creates a new `_worker.js` that re-exports both:
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:
215
235
 
216
236
  ```javascript
217
- export { default } from './_sveltekit_worker.js';
237
+ import sveltekitWorker from './_sveltekit_worker.js';
238
+ import * as extra from './_extra_exports.js';
218
239
  export * from './_extra_exports.js';
240
+ const { fetch: _ignored, ...extraHandlers } = extra.default ?? {};
241
+ export default { ...sveltekitWorker, ...extraHandlers };
219
242
  ```
220
243
 
221
244
  The operation is idempotent -- if `_sveltekit_worker.js` already exists, the plugin skips.
@@ -255,6 +278,18 @@ Note: the `.types-worker-wrangler.jsonc` file is generated when the dev server s
255
278
 
256
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.
257
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.
292
+
258
293
  ## License
259
294
 
260
295
  MIT
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, `export { default } from './_sveltekit_worker.js';\nexport * from './_extra_exports.js';\n`);
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 sidecarName = `${parseJsonc(rawJson).name ?? 'sveltekit'}-dev-worker`;
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 = parseJsonc(rawJson);
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
- await writeFile(tempConfigPath, JSON.stringify(devConfig, null, '\t'));
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 = parseJsonc(rawJson);
167
- if (proxyConfig.durable_objects?.bindings) {
168
- proxyConfig.durable_objects.bindings = proxyConfig.durable_objects.bindings.map((b) => b.script_name ? b : { ...b, script_name: sidecarName });
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 (Array.isArray(proxyConfig.workflows)) {
171
- proxyConfig.workflows = proxyConfig.workflows.map((w) => w.script_name ? w : { ...w, script_name: sidecarName });
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
- delete proxyConfig.migrations;
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
- dev: { registry: getWranglerRegistryPath() },
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
- * Mirrors miniflare's `getNodeCompat` mode resolution. wrangler's
223
- * `unstable_startWorker` does not derive this from the config flags
224
- * automatically — without it, wrangler falls back to the legacy nodejs
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
- const hasCompat = compatibilityFlags.includes('nodejs_compat');
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.4",
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": [