@oselvar/sveltekit-add-worker-exports 2.0.0 → 2.1.1

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A Vite plugin that makes any class-based Cloudflare Worker export (Durable Objects, Workflows, `WorkerEntrypoint` RPC, voice agents, …) and any non-fetch handler (`scheduled`, `queue`, `email`, `tail`, `trace`) work with SvelteKit on Cloudflare, in both dev and production.
4
4
 
5
+ Works with **SvelteKit 2 and SvelteKit 3** (see [`example/`](example) and [`example-v3/`](example-v3)).
6
+
5
7
  **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**, and non-fetch handlers (`scheduled`, `queue`, `email`, …) to be **methods on the default export**. This plugin post-processes the build output to merge both kinds onto SvelteKit's worker.
6
8
 
7
9
  **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. The sidecar also exposes `/__scheduled` so `scheduled` handlers can be fired manually in dev — see [Scheduled, queue, email, tail handlers](#scheduled-queue-email-tail-handlers).
@@ -228,7 +230,7 @@ declare global {
228
230
 
229
231
  ### Build mode
230
232
 
231
- The plugin runs in the `closeBundle` hook (after SvelteKit's adapter has generated `_worker.js`):
233
+ The plugin runs after SvelteKit's adapter has generated `_worker.js`. SvelteKit v2 runs the adapter in Vite's `closeBundle` hook, while SvelteKit v3 moved it into the newer `buildApp` hook (which runs *after* every `closeBundle`). The plugin registers its patch step on **both** hooks (idempotent), so whichever one runs after the adapter does the work:
232
234
 
233
235
  1. Bundles your `entryPoint` with esbuild into `_extra_exports.js`
234
236
  2. Renames the original `_worker.js` to `_sveltekit_worker.js`
@@ -242,7 +244,9 @@ const { fetch: _ignored, ...extraHandlers } = extra.default ?? {};
242
244
  export default { ...sveltekitWorker, ...extraHandlers };
243
245
  ```
244
246
 
245
- The operation is idempotent -- if `_sveltekit_worker.js` already exists, the plugin skips.
247
+ 4. Appends its own outputs (`_sveltekit_worker.js`, `_extra_exports.js`, and their `.map` sidecars) to the adapter's `.assetsignore`. These are server-side / Durable Object bundles and source maps; the adapter's generated `.assetsignore` only excludes its own outputs, so without this Cloudflare would serve them as public static assets (the maps' `sourcesContent` would expose your original TypeScript). `.assetsignore` only controls public asset serving — the files stay on disk, so wrangler still bundles the worker and uploads its source maps.
248
+
249
+ The operation is idempotent -- if `_sveltekit_worker.js` already exists, the second hook skips. Tested against SvelteKit v2 (see [`example/`](example)) and v3 (see [`example-v3/`](example-v3)).
246
250
 
247
251
  ### Dev mode
248
252
 
package/dist/index.d.ts CHANGED
@@ -29,6 +29,23 @@ import type { Plugin } from 'vite';
29
29
  * Exported for tests; the dev plugin uses it via the configureServer hook.
30
30
  */
31
31
  export declare function parseWranglerConfig(path: string, contents: string): unknown;
32
+ /**
33
+ * The build outputs this plugin writes into the adapter's output dir. They are
34
+ * server-side / Durable Object bundles and their source maps — never meant to
35
+ * be downloadable. adapter-cloudflare's generated `.assetsignore` only excludes
36
+ * its own outputs (`_worker.js`, `_routes.json`, `_headers`, `_redirects`), so
37
+ * without this Cloudflare would serve these as public static assets, leaking
38
+ * the bundled code and (via the maps' `sourcesContent`) the original
39
+ * TypeScript. See https://github.com/oselvar/sveltekit-add-worker-exports/issues/4
40
+ */
41
+ export declare const PLUGIN_ASSETS_IGNORE_ENTRIES: string[];
42
+ /**
43
+ * Returns `.assetsignore` content with `entries` appended, skipping any that
44
+ * are already present (trim-compared). Preserves existing lines and order,
45
+ * normalises to a trailing newline. Idempotent — `.assetsignore` controls only
46
+ * public asset serving, so the listed files stay on disk for wrangler to bundle.
47
+ */
48
+ export declare function mergeAssetsIgnore(existing: string, entries: string[]): string;
32
49
  export interface AddWorkerExportsOptions {
33
50
  /** File that exports Durable Object and/or Workflow classes */
34
51
  entryPoint: string;
@@ -38,6 +55,17 @@ export interface AddWorkerExportsOptions {
38
55
  wranglerConfig?: string;
39
56
  /** Port for the dev worker server (default: 8787) */
40
57
  devPort?: number;
58
+ /**
59
+ * Handles each structured log line emitted by the sidecar worker (one entry
60
+ * per `console.log` / `console.error` / etc. call). When omitted, a default
61
+ * handler prints each line to the host's stdout (or stderr for `error`
62
+ * level) prefixed with the sidecar name. Pass `() => {}` to silence.
63
+ */
64
+ structuredLogsHandler?: (log: {
65
+ level: string;
66
+ message: string;
67
+ timestamp: number;
68
+ }) => void;
41
69
  }
42
70
  /**
43
71
  * Returns two Vite plugins: one for build (patches _worker.js) and one for
package/dist/index.js CHANGED
@@ -40,6 +40,37 @@ export function parseWranglerConfig(path, contents) {
40
40
  return path.endsWith('.toml') ? parseToml(contents) : parseJsonc(contents);
41
41
  }
42
42
  const DEFAULT_DEV_PORT = 8787;
43
+ /**
44
+ * The build outputs this plugin writes into the adapter's output dir. They are
45
+ * server-side / Durable Object bundles and their source maps — never meant to
46
+ * be downloadable. adapter-cloudflare's generated `.assetsignore` only excludes
47
+ * its own outputs (`_worker.js`, `_routes.json`, `_headers`, `_redirects`), so
48
+ * without this Cloudflare would serve these as public static assets, leaking
49
+ * the bundled code and (via the maps' `sourcesContent`) the original
50
+ * TypeScript. See https://github.com/oselvar/sveltekit-add-worker-exports/issues/4
51
+ */
52
+ export const PLUGIN_ASSETS_IGNORE_ENTRIES = [
53
+ '_sveltekit_worker.js',
54
+ '_sveltekit_worker.js.map',
55
+ '_extra_exports.js',
56
+ '_extra_exports.js.map'
57
+ ];
58
+ /**
59
+ * Returns `.assetsignore` content with `entries` appended, skipping any that
60
+ * are already present (trim-compared). Preserves existing lines and order,
61
+ * normalises to a trailing newline. Idempotent — `.assetsignore` controls only
62
+ * public asset serving, so the listed files stay on disk for wrangler to bundle.
63
+ */
64
+ export function mergeAssetsIgnore(existing, entries) {
65
+ const lines = existing.split('\n');
66
+ const present = new Set(lines.map((l) => l.trim()));
67
+ const additions = entries.filter((e) => !present.has(e.trim()));
68
+ // Drop trailing blank lines so we append cleanly, then re-add one newline.
69
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
70
+ lines.pop();
71
+ }
72
+ return [...lines, ...additions].join('\n') + '\n';
73
+ }
43
74
  /**
44
75
  * Mirrors wrangler's internal getRegistryPath() — wrangler doesn't export it.
45
76
  * Workers register themselves under this path so cross-worker `script_name`
@@ -76,66 +107,100 @@ function getWranglerRegistryPath() {
76
107
  export function addWorkerExports(options) {
77
108
  return [buildPlugin(options), devPlugin(options)];
78
109
  }
110
+ /**
111
+ * Patches the adapter-generated `_worker.js`: bundles the named exports into
112
+ * `_extra_exports.js`, renames the original worker to `_sveltekit_worker.js`,
113
+ * and writes a merged `_worker.js`. Idempotent — safe to call from more than
114
+ * one Vite hook; the first call that finds `_worker.js` does the work, later
115
+ * calls see `_sveltekit_worker.js` and return.
116
+ *
117
+ * Returns `true` if it patched (or had already patched), `false` if the
118
+ * adapter hasn't written `_worker.js` yet.
119
+ */
120
+ async function patchWorker(options) {
121
+ const outputDir = resolve(options.outputDir ?? '.svelte-kit/cloudflare');
122
+ const workerPath = resolve(outputDir, '_worker.js');
123
+ const sveltekitPath = resolve(outputDir, '_sveltekit_worker.js');
124
+ const exportsPath = resolve(outputDir, '_extra_exports.js');
125
+ // Skip if already patched (idempotent)
126
+ if (await exists(sveltekitPath)) {
127
+ return true;
128
+ }
129
+ // Skip if the adapter hasn't run yet (e.g. during the SSR build phase, or
130
+ // before SvelteKit v3's `buildApp` hook runs the adapter)
131
+ if (!(await exists(workerPath))) {
132
+ return false;
133
+ }
134
+ // Bundle the named exports.
135
+ //
136
+ // `conditions: ['workerd', 'worker', 'browser']` is the Cloudflare-
137
+ // recommended set for bundlers targeting Workers — it picks the
138
+ // workerd-specific or browser-shimmed variants of packages over
139
+ // their Node variants (e.g. nanoid's webcrypto-via-`globalThis.crypto`
140
+ // browser build instead of the `node:crypto`-importing main build).
141
+ //
142
+ // Node built-ins are externalised so transitive deps that do
143
+ // `require('path')` or `import 'node:async_hooks'` survive bundling;
144
+ // the Workers runtime resolves them when `nodejs_compat` is enabled
145
+ // in wrangler.jsonc.
146
+ await build({
147
+ entryPoints: [options.entryPoint],
148
+ bundle: true,
149
+ format: 'esm',
150
+ sourcemap: true,
151
+ target: 'esnext',
152
+ conditions: ['workerd', 'worker', 'browser'],
153
+ external: ['cloudflare:*', ...NODE_BUILTINS, ...NODE_BUILTINS.map((m) => `node:${m}`)],
154
+ outfile: exportsPath
155
+ });
156
+ // Rename original worker and create merged entry point.
157
+ //
158
+ // `export *` re-exports named exports only, so class-based bindings
159
+ // (DOs, Workflows, WorkerEntrypoint) from `_extra_exports.js` flow
160
+ // through. Non-fetch handlers (scheduled, queue, email, tail, trace)
161
+ // live on the user entry's `default` export — Cloudflare invokes
162
+ // them as methods on the worker's default object, not as named
163
+ // exports — so we spread them onto the SvelteKit default. `fetch`
164
+ // is dropped from the user default because SvelteKit owns request
165
+ // handling in production.
166
+ await rename(workerPath, sveltekitPath);
167
+ await writeFile(workerPath, `import sveltekitWorker from './_sveltekit_worker.js';\n` +
168
+ `import * as extra from './_extra_exports.js';\n` +
169
+ `export * from './_extra_exports.js';\n` +
170
+ `const { fetch: _ignored, ...extraHandlers } = extra.default ?? {};\n` +
171
+ `export default { ...sveltekitWorker, ...extraHandlers };\n`);
172
+ // Keep this plugin's server bundles and source maps out of the
173
+ // publicly-served assets. The adapter regenerates `.assetsignore` every
174
+ // build with only its own outputs; we append ours here, right after
175
+ // patching, so it survives each deploy regardless of which hook ran the
176
+ // patch (v2: `closeBundle`, v3: `buildApp`).
177
+ const assetsIgnorePath = resolve(outputDir, '.assetsignore');
178
+ const currentIgnore = (await exists(assetsIgnorePath))
179
+ ? await readFile(assetsIgnorePath, 'utf-8')
180
+ : '';
181
+ await writeFile(assetsIgnorePath, mergeAssetsIgnore(currentIgnore, PLUGIN_ASSETS_IGNORE_ENTRIES));
182
+ return true;
183
+ }
79
184
  function buildPlugin(options) {
80
185
  return {
81
186
  name: 'add-worker-exports',
82
187
  apply: 'build',
83
188
  enforce: 'post',
189
+ // SvelteKit v2 runs the Cloudflare adapter inside `closeBundle`; our
190
+ // `enforce: 'post'` hook then runs after it and sees `_worker.js`.
84
191
  async closeBundle() {
85
- const outputDir = resolve(options.outputDir ?? '.svelte-kit/cloudflare');
86
- const workerPath = resolve(outputDir, '_worker.js');
87
- const sveltekitPath = resolve(outputDir, '_sveltekit_worker.js');
88
- const exportsPath = resolve(outputDir, '_extra_exports.js');
89
- // Skip if already patched (idempotent)
90
- if (await exists(sveltekitPath)) {
91
- return;
92
- }
93
- // Skip if the adapter hasn't run yet (e.g. during SSR build phase)
94
- if (!(await exists(workerPath))) {
95
- return;
192
+ await patchWorker(options);
193
+ },
194
+ // SvelteKit v3 moved the adapter into Vite's `buildApp` hook, which runs
195
+ // *after* every `closeBundle`. By that point our `closeBundle` has
196
+ // already run and skipped (no `_worker.js` yet), so we patch here
197
+ // instead. `order: 'post'` ensures we run after SvelteKit's own
198
+ // `buildApp` that generates `_worker.js`. Harmless on v2 (idempotent).
199
+ buildApp: {
200
+ order: 'post',
201
+ async handler() {
202
+ await patchWorker(options);
96
203
  }
97
- // Bundle the named exports.
98
- //
99
- // `conditions: ['workerd', 'worker', 'browser']` is the Cloudflare-
100
- // recommended set for bundlers targeting Workers — it picks the
101
- // workerd-specific or browser-shimmed variants of packages over
102
- // their Node variants (e.g. nanoid's webcrypto-via-`globalThis.crypto`
103
- // browser build instead of the `node:crypto`-importing main build).
104
- //
105
- // Node built-ins are externalised so transitive deps that do
106
- // `require('path')` or `import 'node:async_hooks'` survive bundling;
107
- // the Workers runtime resolves them when `nodejs_compat` is enabled
108
- // in wrangler.jsonc.
109
- await build({
110
- entryPoints: [options.entryPoint],
111
- bundle: true,
112
- format: 'esm',
113
- sourcemap: true,
114
- target: 'esnext',
115
- conditions: ['workerd', 'worker', 'browser'],
116
- external: [
117
- 'cloudflare:*',
118
- ...NODE_BUILTINS,
119
- ...NODE_BUILTINS.map((m) => `node:${m}`)
120
- ],
121
- outfile: exportsPath
122
- });
123
- // Rename original worker and create merged entry point.
124
- //
125
- // `export *` re-exports named exports only, so class-based bindings
126
- // (DOs, Workflows, WorkerEntrypoint) from `_extra_exports.js` flow
127
- // through. Non-fetch handlers (scheduled, queue, email, tail, trace)
128
- // live on the user entry's `default` export — Cloudflare invokes
129
- // them as methods on the worker's default object, not as named
130
- // exports — so we spread them onto the SvelteKit default. `fetch`
131
- // is dropped from the user default because SvelteKit owns request
132
- // handling in production.
133
- await rename(workerPath, sveltekitPath);
134
- await writeFile(workerPath, `import sveltekitWorker from './_sveltekit_worker.js';\n` +
135
- `import * as extra from './_extra_exports.js';\n` +
136
- `export * from './_extra_exports.js';\n` +
137
- `const { fetch: _ignored, ...extraHandlers } = extra.default ?? {};\n` +
138
- `export default { ...sveltekitWorker, ...extraHandlers };\n`);
139
204
  }
140
205
  };
141
206
  }
@@ -210,6 +275,7 @@ function devPlugin(options) {
210
275
  delete activeScope.migrations;
211
276
  proxyConfigPath = resolve('.platform-proxy-wrangler.jsonc');
212
277
  await writeFile(proxyConfigPath, JSON.stringify(proxyConfig, null, '\t'));
278
+ const structuredLogsHandler = options.structuredLogsHandler ?? makeDefaultWorkerLogHandler(sidecarName);
213
279
  worker = await unstable_startWorker({
214
280
  config: tempConfigPath,
215
281
  // `testScheduled` mounts a `/__scheduled` endpoint on the sidecar
@@ -217,7 +283,19 @@ function devPlugin(options) {
217
283
  // never auto-fires crons. Curl
218
284
  // `http://localhost:<devPort>/__scheduled?cron=*+*+*+*+*` to
219
285
  // trigger your `scheduled` handler.
220
- dev: { registry: getWranglerRegistryPath(), testScheduled: true },
286
+ //
287
+ // `structuredLogsHandler` (added in wrangler 4.99) routes each
288
+ // worker log line through `onWorkerLog`. Without it, wrangler
289
+ // pipes worker logs through its own Logger which can get
290
+ // swallowed when `unstable_startWorker` is driven
291
+ // programmatically — `console.log` inside the worker
292
+ // disappears. Cast is needed because peer wrangler types may
293
+ // be older.
294
+ dev: {
295
+ registry: getWranglerRegistryPath(),
296
+ testScheduled: true,
297
+ structuredLogsHandler
298
+ },
221
299
  build: {
222
300
  nodejsCompatMode: (parsedConfig) => getNodejsCompatMode(parsedConfig.compatibility_date, parsedConfig.compatibility_flags)
223
301
  }
@@ -237,6 +315,18 @@ function devPlugin(options) {
237
315
  }
238
316
  };
239
317
  }
318
+ /**
319
+ * Default handler for sidecar worker logs. Writes each line to stdout (or
320
+ * stderr for `error` level) tagged with the sidecar name so it's obvious
321
+ * which process produced it.
322
+ */
323
+ function makeDefaultWorkerLogHandler(sidecarName) {
324
+ const prefix = `[${sidecarName}]`;
325
+ return ({ level, message }) => {
326
+ const stream = level === 'error' ? process.stderr : process.stdout;
327
+ stream.write(`${prefix} ${message}\n`);
328
+ };
329
+ }
240
330
  async function exists(path) {
241
331
  try {
242
332
  await access(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oselvar/sveltekit-add-worker-exports",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Vite plugin that adds Durable Object and Workflow exports to SvelteKit Cloudflare workers",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -37,15 +37,15 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@release-it/conventional-changelog": "^11.0.1",
40
- "@sveltejs/kit": "^2.62.0",
41
- "@types/node": "^25.9.1",
42
- "esbuild": "^0.28.0",
43
- "miniflare": "^4.20260601.0",
40
+ "@sveltejs/kit": "^2.66.0",
41
+ "@types/node": "^26.0.0",
42
+ "esbuild": "^0.28.1",
43
+ "miniflare": "^4.20260617.1",
44
44
  "release-it": "^20.2.0",
45
45
  "typescript": "^6.0.3",
46
46
  "vite": "^8.0.16",
47
- "vitest": "^4.1.8",
48
- "wrangler": "^4.98.0"
47
+ "vitest": "^4.1.9",
48
+ "wrangler": "^4.103.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsc",