@oselvar/sveltekit-add-worker-exports 2.1.2 → 2.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/README.md CHANGED
@@ -4,6 +4,8 @@ A Vite plugin that makes any class-based Cloudflare Worker export (Durable Objec
4
4
 
5
5
  Works with **SvelteKit 2 and SvelteKit 3** (see [`example/`](example) and [`example-v3/`](example-v3)).
6
6
 
7
+ The examples below use SvelteKit 2's `platform.env`. SvelteKit 3's `adapter-cloudflare` (8.0.0-next.7 and later) no longer provides `event.platform`: read bindings with `import { env } from 'cloudflare:workers'` instead, as [`example-v3/`](example-v3) does. The plugin's dev-registry wiring works the same either way.
8
+
7
9
  **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.
8
10
 
9
11
  **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).
@@ -302,25 +304,29 @@ The plugin reads your wrangler config, creates a temporary config with `main` po
302
304
 
303
305
  ### Generating typed bindings
304
306
 
305
- The dev plugin creates a temporary `.dev-worker-wrangler.jsonc` with `main` pointing to your source entry point. Use it to generate fully generic Cloudflare types:
307
+ The dev plugin runs the sidecar from a derived `.dev-worker-wrangler.jsonc`: your wrangler config with `main` pointing to your source entry point and `assets` removed. Use it to generate fully generic Cloudflare types:
306
308
 
307
309
  ```bash
310
+ npx sveltekit-add-worker-exports write-config
308
311
  wrangler types --config .dev-worker-wrangler.jsonc
309
312
  ```
310
313
 
311
- This produces typed DO bindings like `DurableObjectNamespace<MyDurableObject>` instead of the untyped `DurableObjectNamespace` you get from the default `wrangler.jsonc` (whose `main` points to the SvelteKit build output, which doesn't exist during dev).
314
+ This produces typed DO bindings like `DurableObjectNamespace<MyDurableObject>` instead of the untyped `DurableObjectNamespace` you get from the default `wrangler.jsonc`, whose `main` points to the SvelteKit build output (which also drags `.svelte-kit/cloudflare/_worker` into `tsc` via `mainModule`).
312
315
 
313
- Add this to your `package.json` scripts for convenience:
316
+ `write-config` writes the same file the dev server does, without starting it. It reads `entryPoint` (and `wranglerConfig`, if set) from the `addWorkerExports()` call in your `vite.config.ts`, so it works in CI. Add scripts to `package.json`:
314
317
 
315
318
  ```json
316
319
  {
317
320
  "scripts": {
318
- "types": "wrangler types --config .dev-worker-wrangler.jsonc"
321
+ "types": "sveltekit-add-worker-exports write-config && wrangler types --config .dev-worker-wrangler.jsonc",
322
+ "types:check": "sveltekit-add-worker-exports write-config && wrangler types --config .dev-worker-wrangler.jsonc --check"
319
323
  }
320
324
  }
321
325
  ```
322
326
 
323
- Note: the `.dev-worker-wrangler.jsonc` file is generated when the dev server starts. Run `pnpm dev` at least once before running `wrangler types`.
327
+ Commit `worker-configuration.d.ts` and run `types:check` in CI to catch stale types. Add `.dev-worker-wrangler.jsonc` to `.gitignore`.
328
+
329
+ Options: `--out <path>` changes the output file, `--vite-config <path>` points at a non-default vite config, and `--entry <path>` (with optional `--wrangler-config <path>`) skips loading the vite config altogether. From code, use `writeDevWorkerConfig({ entryPoint })` or the pure `deriveDevWorkerConfig(config, { entryPoint })`.
324
330
 
325
331
  ## Why this exists
326
332
 
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ // Committed launcher (not built) so package managers can link the bin
3
+ // before `dist/` exists, e.g. in this workspace right after `pnpm install`.
4
+ import '../dist/bin.js';
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,9 @@
1
+ import { main, USAGE, UsageError } from './cli.js';
2
+ main(process.argv.slice(2)).catch((error) => {
3
+ const message = error instanceof Error ? error.message : String(error);
4
+ process.stderr.write(`sveltekit-add-worker-exports: ${message}\n`);
5
+ if (error instanceof UsageError) {
6
+ process.stderr.write(`\n${USAGE}`);
7
+ }
8
+ process.exitCode = 1;
9
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * CLI for tasks that shouldn't require a running `vite dev`.
3
+ *
4
+ * sveltekit-add-worker-exports write-config [options]
5
+ *
6
+ * Writes `.dev-worker-wrangler.jsonc` (the config the dev sidecar runs) so
7
+ * `wrangler types --config .dev-worker-wrangler.jsonc [--check]` works in CI.
8
+ * The plugin options are read from the vite config by default; `--entry`
9
+ * skips loading it.
10
+ */
11
+ import { type AddWorkerExportsOptions } from './index.js';
12
+ export declare const USAGE = "Usage: sveltekit-add-worker-exports write-config [options]\n\nWrites .dev-worker-wrangler.jsonc for `wrangler types --config .dev-worker-wrangler.jsonc`\nwithout starting the dev server.\n\nOptions:\n --out <path> Output file (default: .dev-worker-wrangler.jsonc)\n --vite-config <path> Vite config to read plugin options from (default: auto-discovered)\n --entry <path> Worker entry point; skips loading the vite config\n --wrangler-config <path> Wrangler config (default: from the vite config, else auto-discovered)\n -h, --help Show this help\n";
13
+ export declare function main(argv: string[]): Promise<void>;
14
+ /**
15
+ * Loads the vite config and returns the options passed to
16
+ * `addWorkerExports()`, which the dev plugin exposes as `api.options`.
17
+ */
18
+ export declare function loadPluginOptions(configFile?: string): Promise<AddWorkerExportsOptions>;
19
+ export declare class UsageError extends Error {
20
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * CLI for tasks that shouldn't require a running `vite dev`.
3
+ *
4
+ * sveltekit-add-worker-exports write-config [options]
5
+ *
6
+ * Writes `.dev-worker-wrangler.jsonc` (the config the dev sidecar runs) so
7
+ * `wrangler types --config .dev-worker-wrangler.jsonc [--check]` works in CI.
8
+ * The plugin options are read from the vite config by default; `--entry`
9
+ * skips loading it.
10
+ */
11
+ import { relative } from 'node:path';
12
+ import { parseArgs } from 'node:util';
13
+ import { DEV_WORKER_CONFIG_FILE, writeDevWorkerConfig } from './index.js';
14
+ export const USAGE = `Usage: sveltekit-add-worker-exports write-config [options]
15
+
16
+ Writes ${DEV_WORKER_CONFIG_FILE} for \`wrangler types --config ${DEV_WORKER_CONFIG_FILE}\`
17
+ without starting the dev server.
18
+
19
+ Options:
20
+ --out <path> Output file (default: ${DEV_WORKER_CONFIG_FILE})
21
+ --vite-config <path> Vite config to read plugin options from (default: auto-discovered)
22
+ --entry <path> Worker entry point; skips loading the vite config
23
+ --wrangler-config <path> Wrangler config (default: from the vite config, else auto-discovered)
24
+ -h, --help Show this help
25
+ `;
26
+ export async function main(argv) {
27
+ const { positionals, values } = parseArgs({
28
+ args: argv,
29
+ allowPositionals: true,
30
+ options: {
31
+ out: { type: 'string' },
32
+ 'vite-config': { type: 'string' },
33
+ entry: { type: 'string' },
34
+ 'wrangler-config': { type: 'string' },
35
+ help: { type: 'boolean', short: 'h' }
36
+ }
37
+ });
38
+ if (values.help) {
39
+ process.stdout.write(USAGE);
40
+ return;
41
+ }
42
+ if (positionals[0] !== 'write-config' || positionals.length > 1) {
43
+ throw new UsageError(positionals.length ? `Unknown command: ${positionals.join(' ')}` : 'No command given');
44
+ }
45
+ const options = values.entry
46
+ ? { entryPoint: values.entry }
47
+ : await loadPluginOptions(values['vite-config']);
48
+ if (values['wrangler-config']) {
49
+ options.wranglerConfig = values['wrangler-config'];
50
+ }
51
+ const path = await writeDevWorkerConfig(options, values.out);
52
+ process.stdout.write(`Wrote ${relative(process.cwd(), path) || path}\n`);
53
+ }
54
+ /**
55
+ * Loads the vite config and returns the options passed to
56
+ * `addWorkerExports()`, which the dev plugin exposes as `api.options`.
57
+ */
58
+ export async function loadPluginOptions(configFile) {
59
+ const { loadConfigFromFile } = await import('vite');
60
+ const loaded = await loadConfigFromFile({ command: 'serve', mode: 'development' }, configFile, process.cwd(), 'silent');
61
+ if (!loaded) {
62
+ throw new Error('No vite config found. Pass --vite-config <path> or --entry <path>.');
63
+ }
64
+ const plugins = await flattenPlugins(loaded.config.plugins ?? []);
65
+ const plugin = plugins.find((p) => p.name === 'add-worker-exports-dev');
66
+ const options = plugin?.api?.options;
67
+ if (!options) {
68
+ throw new Error(`addWorkerExports() not found in ${relative(process.cwd(), loaded.path)}. Pass --entry <path> instead.`);
69
+ }
70
+ return { ...options };
71
+ }
72
+ async function flattenPlugins(options) {
73
+ const plugins = [];
74
+ for (const option of options) {
75
+ const resolved = await option;
76
+ if (Array.isArray(resolved)) {
77
+ plugins.push(...(await flattenPlugins(resolved)));
78
+ }
79
+ else if (resolved) {
80
+ plugins.push(resolved);
81
+ }
82
+ }
83
+ return plugins;
84
+ }
85
+ export class UsageError extends Error {
86
+ }
package/dist/index.d.ts CHANGED
@@ -48,6 +48,15 @@ export declare const PLUGIN_ASSETS_IGNORE_ENTRIES: string[];
48
48
  * public asset serving, so the listed files stay on disk for wrangler to bundle.
49
49
  */
50
50
  export declare function mergeAssetsIgnore(existing: string, entries: string[]): string;
51
+ /**
52
+ * Deletes `name`'s dev-registry entry if nothing is listening on its debug
53
+ * port. A sidecar that doesn't shut down cleanly (killed, crashed) leaves its
54
+ * entry behind, and the next sidecar with the same name doesn't replace it:
55
+ * `platform.env` calls then go to the dead address and fail with "Network
56
+ * connection lost" until the file is deleted. A live entry (e.g. another
57
+ * `vite dev` of the same app) is left alone.
58
+ */
59
+ export declare function removeDeadRegistryEntry(registryPath: string, name: string): Promise<void>;
51
60
  export interface AddWorkerExportsOptions {
52
61
  /** File that exports Durable Object and/or Workflow classes */
53
62
  entryPoint: string;
@@ -107,3 +116,33 @@ export declare function addWorkerExports(options: AddWorkerExportsOptions): Plug
107
116
  * new requirement.
108
117
  */
109
118
  export declare function readModuleRules(options: AddWorkerExportsOptions): Promise<ModuleRule[]>;
119
+ /** Default file name for the derived dev-worker wrangler config. */
120
+ export declare const DEV_WORKER_CONFIG_FILE = ".dev-worker-wrangler.jsonc";
121
+ /**
122
+ * Reads and parses the raw wrangler config (auto-discovered, or
123
+ * `wranglerConfig` when set). Returns the unresolved file contents — not
124
+ * wrangler's normalised config — so derived configs stay close to what the
125
+ * user wrote.
126
+ */
127
+ export declare function readWranglerConfig(wranglerConfig?: string): Promise<{
128
+ path: string;
129
+ config: any;
130
+ }>;
131
+ /**
132
+ * Derives the wrangler config the dev sidecar runs from the user's config:
133
+ * `main` points at the source entry point, the name gets a `-dev-worker`
134
+ * suffix, the dev port is set, and `assets` is dropped.
135
+ *
136
+ * The same config is what `wrangler types` should read: generating from the
137
+ * user's config (whose `main` is the SvelteKit build output) makes wrangler
138
+ * emit `mainModule: typeof import(".svelte-kit/cloudflare/_worker")` and
139
+ * untyped Durable Object bindings.
140
+ */
141
+ export declare function deriveDevWorkerConfig(baseConfig: any, options: Pick<AddWorkerExportsOptions, 'entryPoint' | 'devPort'>): any;
142
+ /**
143
+ * Reads the wrangler config, derives the dev-worker config and writes it to
144
+ * `outPath`. Lets `wrangler types --config .dev-worker-wrangler.jsonc` run
145
+ * without starting `vite dev` (e.g. in CI, with `--check`). Returns the
146
+ * absolute path written.
147
+ */
148
+ export declare function writeDevWorkerConfig(options: AddWorkerExportsOptions, outPath?: string): Promise<string>;
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import { build } from 'esbuild';
24
24
  import { existsSync } from 'node:fs';
25
25
  import { access, readFile, rename, writeFile, unlink } from 'node:fs/promises';
26
26
  import { builtinModules as NODE_BUILTINS } from 'node:module';
27
+ import { connect } from 'node:net';
27
28
  import { homedir } from 'node:os';
28
29
  import { join, resolve } from 'node:path';
29
30
  import { parse as parseJsonc } from 'jsonc-parser';
@@ -92,6 +93,42 @@ function getWranglerRegistryPath() {
92
93
  }
93
94
  return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), '.wrangler', 'registry');
94
95
  }
96
+ /**
97
+ * Deletes `name`'s dev-registry entry if nothing is listening on its debug
98
+ * port. A sidecar that doesn't shut down cleanly (killed, crashed) leaves its
99
+ * entry behind, and the next sidecar with the same name doesn't replace it:
100
+ * `platform.env` calls then go to the dead address and fail with "Network
101
+ * connection lost" until the file is deleted. A live entry (e.g. another
102
+ * `vite dev` of the same app) is left alone.
103
+ */
104
+ export async function removeDeadRegistryEntry(registryPath, name) {
105
+ const entryPath = join(registryPath, name);
106
+ let address;
107
+ try {
108
+ address = JSON.parse(await readFile(entryPath, 'utf-8')).debugPortAddress;
109
+ }
110
+ catch {
111
+ return; // no entry, or one we can't parse -- leave it to wrangler
112
+ }
113
+ if (typeof address === 'string' && (await isListening(address)))
114
+ return;
115
+ await unlink(entryPath).catch(() => { });
116
+ }
117
+ function isListening(address, timeoutMs = 1_000) {
118
+ const separator = address.lastIndexOf(':');
119
+ const host = address.slice(0, separator);
120
+ const port = Number(address.slice(separator + 1));
121
+ return new Promise((resolve) => {
122
+ const socket = connect({ host, port });
123
+ const done = (listening) => {
124
+ socket.destroy();
125
+ resolve(listening);
126
+ };
127
+ socket.setTimeout(timeoutMs, () => done(false));
128
+ socket.once('connect', () => done(true));
129
+ socket.once('error', () => done(false));
130
+ });
131
+ }
95
132
  /**
96
133
  * Returns two Vite plugins: one for build (patches _worker.js) and one for
97
134
  * dev (starts a wrangler dev server for Durable Objects).
@@ -107,7 +144,7 @@ function getWranglerRegistryPath() {
107
144
  * ```
108
145
  */
109
146
  export function addWorkerExports(options) {
110
- return [buildPlugin(options), devPlugin(options)];
147
+ return [buildPlugin(options), ...devPlugins(options)];
111
148
  }
112
149
  /**
113
150
  * Patches the adapter-generated `_worker.js`: bundles the named exports into
@@ -232,118 +269,191 @@ function buildPlugin(options) {
232
269
  }
233
270
  };
234
271
  }
235
- function devPlugin(options) {
272
+ /** Default file name for the derived dev-worker wrangler config. */
273
+ export const DEV_WORKER_CONFIG_FILE = '.dev-worker-wrangler.jsonc';
274
+ /**
275
+ * Reads and parses the raw wrangler config (auto-discovered, or
276
+ * `wranglerConfig` when set). Returns the unresolved file contents — not
277
+ * wrangler's normalised config — so derived configs stay close to what the
278
+ * user wrote.
279
+ */
280
+ export async function readWranglerConfig(wranglerConfig) {
281
+ // Dynamic import — see readModuleRules for why.
282
+ const { unstable_readConfig } = await import('wrangler');
283
+ const parsed = unstable_readConfig(wranglerConfig ? { config: wranglerConfig } : {});
284
+ const path = parsed.configPath;
285
+ if (!path) {
286
+ throw new Error(wranglerConfig
287
+ ? `Could not read wrangler config at ${wranglerConfig}`
288
+ : 'No wrangler config (wrangler.jsonc, wrangler.json or wrangler.toml) found');
289
+ }
290
+ const contents = await readFile(path, 'utf-8');
291
+ return { path, config: parseWranglerConfig(path, contents) };
292
+ }
293
+ /**
294
+ * Derives the wrangler config the dev sidecar runs from the user's config:
295
+ * `main` points at the source entry point, the name gets a `-dev-worker`
296
+ * suffix, the dev port is set, and `assets` is dropped.
297
+ *
298
+ * The same config is what `wrangler types` should read: generating from the
299
+ * user's config (whose `main` is the SvelteKit build output) makes wrangler
300
+ * emit `mainModule: typeof import(".svelte-kit/cloudflare/_worker")` and
301
+ * untyped Durable Object bindings.
302
+ */
303
+ export function deriveDevWorkerConfig(baseConfig, options) {
304
+ const devConfig = structuredClone(baseConfig);
305
+ devConfig.name = devWorkerName(baseConfig);
306
+ devConfig.main = options.entryPoint;
307
+ devConfig.dev = { ...devConfig.dev, port: options.devPort ?? DEFAULT_DEV_PORT };
308
+ delete devConfig.assets;
309
+ return devConfig;
310
+ }
311
+ /**
312
+ * Reads the wrangler config, derives the dev-worker config and writes it to
313
+ * `outPath`. Lets `wrangler types --config .dev-worker-wrangler.jsonc` run
314
+ * without starting `vite dev` (e.g. in CI, with `--check`). Returns the
315
+ * absolute path written.
316
+ */
317
+ export async function writeDevWorkerConfig(options, outPath = DEV_WORKER_CONFIG_FILE) {
318
+ const { config } = await readWranglerConfig(options.wranglerConfig);
319
+ const path = resolve(outPath);
320
+ await writeFile(path, JSON.stringify(deriveDevWorkerConfig(config, options), null, '\t'));
321
+ return path;
322
+ }
323
+ function devWorkerName(baseConfig) {
324
+ return `${baseConfig.name ?? 'sveltekit'}-dev-worker`;
325
+ }
326
+ function devPlugins(options) {
236
327
  const devPort = options.devPort ?? DEFAULT_DEV_PORT;
237
328
  let worker = null;
238
329
  let tempConfigPath = null;
239
330
  let proxyConfigPath = null;
240
- async function readRawConfig() {
241
- // Dynamic import — see readModuleRules for why.
242
- const { unstable_readConfig } = await import('wrangler');
243
- const parsed = unstable_readConfig(options.wranglerConfig ? { config: options.wranglerConfig } : {});
244
- const path = parsed.configPath;
245
- const contents = await readFile(path, 'utf-8');
246
- return { path, contents };
331
+ let sidecarName = '';
332
+ let registeredSidecarName = '';
333
+ /**
334
+ * Writes `.dev-worker-wrangler.jsonc` (the sidecar's config) and
335
+ * `.platform-proxy-wrangler.jsonc` (for adapter-cloudflare's
336
+ * `getPlatformProxy`).
337
+ */
338
+ async function writeConfigs() {
339
+ const { config: baseConfig } = await readWranglerConfig(options.wranglerConfig);
340
+ sidecarName = devWorkerName(baseConfig);
341
+ // When CLOUDFLARE_ENV is set, wrangler suffixes the registered
342
+ // worker name with `-${env}` (see appendEnvName in wrangler), so
343
+ // cross-worker `script_name` lookups must use the suffixed name.
344
+ // We don't suffix `devConfig.name` itself — wrangler does that
345
+ // based on CLOUDFLARE_ENV when starting the sidecar.
346
+ const envName = process.env.CLOUDFLARE_ENV;
347
+ registeredSidecarName = envName ? `${sidecarName}-${envName}` : sidecarName;
348
+ // The user can also pass this file to `wrangler types --config
349
+ // .dev-worker-wrangler.jsonc` to get typed bindings like
350
+ // DurableObjectNamespace<EchoDO> resolved from the source entry.
351
+ // Outside of dev, `writeDevWorkerConfig` writes the same file.
352
+ const devConfig = deriveDevWorkerConfig(baseConfig, options);
353
+ tempConfigPath = resolve(DEV_WORKER_CONFIG_FILE);
354
+ await writeFile(tempConfigPath, JSON.stringify(devConfig, null, '\t'));
355
+ // Write a wrangler config for adapter-cloudflare's getPlatformProxy
356
+ // (used by vite dev for platform.env). It can't run internal DOs or
357
+ // Workflows itself -- those are served by the sidecar above. Rewrite
358
+ // internal DO and Workflow bindings as cross-worker bindings
359
+ // (script_name pointing at the sidecar) so platform.env.MY_DO and
360
+ // platform.env.MY_WORKFLOW calls in +server.ts reach the sidecar via
361
+ // the wrangler dev registry.
362
+ const proxyConfig = structuredClone(baseConfig);
363
+ // Wrangler does not merge per-env `durable_objects`/`workflows`/
364
+ // `migrations` arrays with the top-level — the selected env wholly
365
+ // overrides them. So we patch only the scope wrangler will actually
366
+ // use: top-level when no env, env[envName] when CLOUDFLARE_ENV is set.
367
+ const activeScope = envName ? proxyConfig.env?.[envName] : proxyConfig;
368
+ if (envName && !activeScope) {
369
+ throw new Error(`CLOUDFLARE_ENV="${envName}" but no \`env.${envName}\` block in the wrangler config`);
370
+ }
371
+ if (activeScope.durable_objects?.bindings) {
372
+ activeScope.durable_objects.bindings = activeScope.durable_objects.bindings.map((b) => b.script_name ? b : { ...b, script_name: registeredSidecarName });
373
+ }
374
+ if (Array.isArray(activeScope.workflows)) {
375
+ activeScope.workflows = activeScope.workflows.map((w) => w.script_name ? w : { ...w, script_name: registeredSidecarName });
376
+ }
377
+ delete activeScope.migrations;
378
+ proxyConfigPath = resolve('.platform-proxy-wrangler.jsonc');
379
+ await writeFile(proxyConfigPath, JSON.stringify(proxyConfig, null, '\t'));
247
380
  }
248
- return {
249
- name: 'add-worker-exports-dev',
250
- apply: 'serve',
251
- config() {
252
- return {
253
- define: {
254
- __DEV_WORKER_PORT__: JSON.stringify(devPort)
255
- }
256
- };
257
- },
258
- async configureServer(server) {
259
- // Dynamic import — see readModuleRules for why.
260
- const { unstable_startWorker } = await import('wrangler');
261
- const { path: configPath, contents } = await readRawConfig();
262
- const baseConfig = parseWranglerConfig(configPath, contents);
263
- const sidecarName = `${baseConfig.name ?? 'sveltekit'}-dev-worker`;
264
- // When CLOUDFLARE_ENV is set, wrangler suffixes the registered
265
- // worker name with `-${env}` (see appendEnvName in wrangler), so
266
- // cross-worker `script_name` lookups must use the suffixed name.
267
- // We don't suffix `devConfig.name` itself — wrangler does that
268
- // based on CLOUDFLARE_ENV when starting the sidecar.
269
- const envName = process.env.CLOUDFLARE_ENV;
270
- const registeredSidecarName = envName ? `${sidecarName}-${envName}` : sidecarName;
271
- // Copy the raw config and override main, name, and dev port
272
- const devConfig = structuredClone(baseConfig);
273
- devConfig.name = sidecarName;
274
- devConfig.main = options.entryPoint;
275
- devConfig.dev = { ...devConfig.dev, port: devPort };
276
- delete devConfig.assets;
277
- // The user can also pass this file to `wrangler types --config
278
- // .dev-worker-wrangler.jsonc` to get typed bindings like
279
- // DurableObjectNamespace<EchoDO> resolved from the source entry.
280
- const devConfigJson = JSON.stringify(devConfig, null, '\t');
281
- tempConfigPath = resolve('.dev-worker-wrangler.jsonc');
282
- await writeFile(tempConfigPath, devConfigJson);
283
- // Write a wrangler config for adapter-cloudflare's getPlatformProxy
284
- // (used by vite dev for platform.env). It can't run internal DOs or
285
- // Workflows itself -- those are served by the sidecar above. Rewrite
286
- // internal DO and Workflow bindings as cross-worker bindings
287
- // (script_name pointing at the sidecar) so platform.env.MY_DO and
288
- // platform.env.MY_WORKFLOW calls in +server.ts reach the sidecar via
289
- // the wrangler dev registry.
290
- const proxyConfig = structuredClone(baseConfig);
291
- // Wrangler does not merge per-env `durable_objects`/`workflows`/
292
- // `migrations` arrays with the top-level — the selected env wholly
293
- // overrides them. So we patch only the scope wrangler will actually
294
- // use: top-level when no env, env[envName] when CLOUDFLARE_ENV is set.
295
- const activeScope = envName ? proxyConfig.env?.[envName] : proxyConfig;
296
- if (envName && !activeScope) {
297
- throw new Error(`CLOUDFLARE_ENV="${envName}" but no \`env.${envName}\` block in the wrangler config`);
381
+ return [
382
+ // Writes the config files before other plugins' dev-server hooks run.
383
+ // adapter-cloudflare 8 calls `getPlatformProxy` from its own
384
+ // `configureServer`, which reads `.platform-proxy-wrangler.jsonc` and
385
+ // fails on a fresh checkout if we haven't written it yet. Vite awaits
386
+ // these hooks in order.
387
+ {
388
+ name: 'add-worker-exports-dev-config',
389
+ apply: 'serve',
390
+ configureServer: {
391
+ order: 'pre',
392
+ handler: writeConfigs
298
393
  }
299
- if (activeScope.durable_objects?.bindings) {
300
- activeScope.durable_objects.bindings = activeScope.durable_objects.bindings.map((b) => b.script_name ? b : { ...b, script_name: registeredSidecarName });
301
- }
302
- if (Array.isArray(activeScope.workflows)) {
303
- activeScope.workflows = activeScope.workflows.map((w) => w.script_name ? w : { ...w, script_name: registeredSidecarName });
394
+ },
395
+ // Starts the sidecar in normal order, after SvelteKit's hooks. Starting
396
+ // it earlier makes wrangler reload it when SvelteKit regenerates files
397
+ // the bundle watches (e.g. the tsconfig chain), dropping in-flight work
398
+ // such as a just-created Workflow.
399
+ {
400
+ name: 'add-worker-exports-dev',
401
+ apply: 'serve',
402
+ // Lets the CLI (`sveltekit-add-worker-exports write-config`) find the
403
+ // plugin options in a loaded vite config.
404
+ api: { options },
405
+ config() {
406
+ return {
407
+ define: {
408
+ __DEV_WORKER_PORT__: JSON.stringify(devPort)
409
+ }
410
+ };
411
+ },
412
+ async configureServer(server) {
413
+ // Dynamic import — see readModuleRules for why.
414
+ const { unstable_startWorker } = await import('wrangler');
415
+ const structuredLogsHandler = options.structuredLogsHandler ?? makeDefaultWorkerLogHandler(sidecarName);
416
+ const registryPath = getWranglerRegistryPath();
417
+ await removeDeadRegistryEntry(registryPath, registeredSidecarName);
418
+ worker = await unstable_startWorker({
419
+ config: tempConfigPath,
420
+ // `testScheduled` mounts a `/__scheduled` endpoint on the sidecar
421
+ // so cron handlers can be invoked manually in dev — wrangler dev
422
+ // never auto-fires crons. Curl
423
+ // `http://localhost:<devPort>/__scheduled?cron=*+*+*+*+*` to
424
+ // trigger your `scheduled` handler.
425
+ //
426
+ // `structuredLogsHandler` (added in wrangler 4.99) routes each
427
+ // worker log line through `onWorkerLog`. Without it, wrangler
428
+ // pipes worker logs through its own Logger which can get
429
+ // swallowed when `unstable_startWorker` is driven
430
+ // programmatically — `console.log` inside the worker
431
+ // disappears. Cast is needed because peer wrangler types may
432
+ // be older.
433
+ dev: {
434
+ registry: registryPath,
435
+ testScheduled: true,
436
+ structuredLogsHandler
437
+ },
438
+ build: {
439
+ nodejsCompatMode: (parsedConfig) => getNodejsCompatMode(parsedConfig.compatibility_date, parsedConfig.compatibility_flags)
440
+ }
441
+ });
442
+ server.httpServer?.on('close', async () => {
443
+ if (worker) {
444
+ await worker.dispose();
445
+ worker = null;
446
+ }
447
+ if (tempConfigPath) {
448
+ await unlink(tempConfigPath).catch(() => { });
449
+ }
450
+ if (proxyConfigPath) {
451
+ await unlink(proxyConfigPath).catch(() => { });
452
+ }
453
+ });
304
454
  }
305
- delete activeScope.migrations;
306
- proxyConfigPath = resolve('.platform-proxy-wrangler.jsonc');
307
- await writeFile(proxyConfigPath, JSON.stringify(proxyConfig, null, '\t'));
308
- const structuredLogsHandler = options.structuredLogsHandler ?? makeDefaultWorkerLogHandler(sidecarName);
309
- worker = await unstable_startWorker({
310
- config: tempConfigPath,
311
- // `testScheduled` mounts a `/__scheduled` endpoint on the sidecar
312
- // so cron handlers can be invoked manually in dev — wrangler dev
313
- // never auto-fires crons. Curl
314
- // `http://localhost:<devPort>/__scheduled?cron=*+*+*+*+*` to
315
- // trigger your `scheduled` handler.
316
- //
317
- // `structuredLogsHandler` (added in wrangler 4.99) routes each
318
- // worker log line through `onWorkerLog`. Without it, wrangler
319
- // pipes worker logs through its own Logger which can get
320
- // swallowed when `unstable_startWorker` is driven
321
- // programmatically — `console.log` inside the worker
322
- // disappears. Cast is needed because peer wrangler types may
323
- // be older.
324
- dev: {
325
- registry: getWranglerRegistryPath(),
326
- testScheduled: true,
327
- structuredLogsHandler
328
- },
329
- build: {
330
- nodejsCompatMode: (parsedConfig) => getNodejsCompatMode(parsedConfig.compatibility_date, parsedConfig.compatibility_flags)
331
- }
332
- });
333
- server.httpServer?.on('close', async () => {
334
- if (worker) {
335
- await worker.dispose();
336
- worker = null;
337
- }
338
- if (tempConfigPath) {
339
- await unlink(tempConfigPath).catch(() => { });
340
- }
341
- if (proxyConfigPath) {
342
- await unlink(proxyConfigPath).catch(() => { });
343
- }
344
- });
345
455
  }
346
- };
456
+ ];
347
457
  }
348
458
  /**
349
459
  * Default handler for sidecar worker logs. Writes each line to stdout (or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oselvar/sveltekit-add-worker-exports",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Vite plugin that adds Durable Object and Workflow exports to SvelteKit Cloudflare workers",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -12,7 +12,11 @@
12
12
  "import": "./dist/index.js"
13
13
  }
14
14
  },
15
+ "bin": {
16
+ "sveltekit-add-worker-exports": "./bin/sveltekit-add-worker-exports.js"
17
+ },
15
18
  "files": [
19
+ "bin",
16
20
  "dist"
17
21
  ],
18
22
  "license": "MIT",
@@ -37,15 +41,15 @@
37
41
  },
38
42
  "devDependencies": {
39
43
  "@release-it/conventional-changelog": "^11.0.1",
40
- "@sveltejs/kit": "^2.69.3",
41
- "@types/node": "^26.1.1",
42
- "esbuild": "^0.28.1",
43
- "miniflare": "^4.20260710.0",
44
+ "@sveltejs/kit": "^2.70.3",
45
+ "@types/node": "^26.6.2",
46
+ "esbuild": "^0.28.2",
47
+ "miniflare": "^4.20260730.0",
44
48
  "release-it": "^20.2.1",
45
49
  "typescript": "^7.0.2",
46
- "vite": "^8.1.4",
47
- "vitest": "^4.1.10",
48
- "wrangler": "^4.111.0"
50
+ "vite": "^8.3.0",
51
+ "vitest": "^4.1.11",
52
+ "wrangler": "^4.136.2"
49
53
  },
50
54
  "scripts": {
51
55
  "build": "tsc",
@@ -64,6 +68,6 @@
64
68
  },
65
69
  "dependencies": {
66
70
  "jsonc-parser": "^3.3.1",
67
- "smol-toml": "^1.7.0"
71
+ "smol-toml": "^1.8.0"
68
72
  }
69
73
  }