npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oselvar/sveltekit-add-worker-exports

v2.1.2

Published

Vite plugin that adds Durable Object and Workflow exports to SvelteKit Cloudflare workers

Readme

@oselvar/sveltekit-add-worker-exports

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.

Works with SvelteKit 2 and SvelteKit 3 (see example/ and example-v3/).

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.

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 below. The sidecar also exposes /__scheduled so scheduled handlers can be fired manually in dev — see Scheduled, queue, email, tail handlers.

What this works with

The plugin is binding-agnostic: if Cloudflare resolves it through a named class export plus a wrangler.jsonc binding, or through a method on the default export, the plugin handles it. Confirmed working:

It should also work with WorkerEntrypoint RPC and any future class-based export that follows the same export-plus-binding pattern, as well as queue, email, tail, and trace — anything Cloudflare invokes as a method on the worker's default export. 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.

Install

pnpm add -D @oselvar/sveltekit-add-worker-exports

esbuild, vite, and wrangler are peer dependencies -- your SvelteKit project already has them.

Usage

Create a worker entry point that exports your class-based bindings:

// src/lib/server/index.ts
export { MyDurableObject } from './MyDurableObject';
export { MyWorkflow } from './MyWorkflow';

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.

If clients need to talk directly to the dev sidecar over HTTP (e.g. WebSockets), also export a default fetch handler — see WebSockets below.

Add the plugin to your vite.config.ts after sveltekit():

import { sveltekit } from '@sveltejs/kit/vite';
import { addWorkerExports } from '@oselvar/sveltekit-add-worker-exports';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    sveltekit(),
    addWorkerExports({ entryPoint: 'src/lib/server/index.ts' })
  ]
});

Point adapter-cloudflare's platform proxy at the generated .platform-proxy-wrangler.jsonc. The plugin writes this file with internal Durable Object and Workflow bindings rewritten to cross-worker form (each gets a script_name pointing at the sidecar), so platform.env.MY_DO and platform.env.MY_WORKFLOW calls in +server.ts reach the sidecar via the wrangler dev registry. Without this config path, getPlatformProxy would try to run the DO/Workflow classes itself and warn that it can't:

// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter({
      platformProxy: {
        configPath: '.platform-proxy-wrangler.jsonc'
      }
    })
  }
};

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.

Calling Workflows

+server.ts calls workflows the same way in dev and production:

export const POST: RequestHandler = async ({ params, request, platform }) => {
  const userMessage = await request.text();
  const instance = await platform!.env.MY_WORKFLOW.create({
    params: { ... }
  });
  return new Response(instance.id);
};

The sidecar runs the real WorkflowEntrypoint class; calls reach it via the script_name rewrite in the platform-proxy config. This requires wrangler >= 4.98.0 (cloudflare/workers-sdk#13863).

Testing the production build locally

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:

pnpm build              # produces .svelte-kit/cloudflare/_worker.js with merged exports
pnpm wrangler dev       # uses wrangler.jsonc → main: .svelte-kit/cloudflare/_worker.js

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.

A handy shortcut is to add a preview script to package.json:

{
  "scripts": {
    "preview": "wrangler dev"
  }
}

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.

Scheduled, queue, email, tail handlers

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:

// src/lib/server/index.ts
export { MyDurableObject } from './MyDurableObject';
export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    // runs from the cron triggers in wrangler.jsonc
  }
};

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 below for the full shape.

Firing scheduled in dev

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:

curl 'http://localhost:8787/__scheduled?cron=*+*+*+*+*'

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.

WebSockets

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.

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.

// src/lib/server/forwardWebSocket.ts
export async function forwardWebSocket<T extends Rpc.DurableObjectBranded | undefined>(
  request: Request,
  namespace: DurableObjectNamespace<T>,
  name: string
): Promise<Response> {
  if (request.headers.get('upgrade') !== 'websocket') {
    return new Response('Expected WebSocket', { status: 426 });
  }
  const id = namespace.idFromName(name);
  return namespace.get(id).fetch(request);
}

Put a fetch on the entry's default export so the dev sidecar can receive the direct connection:

// src/lib/server/index.ts
import { forwardWebSocket } from './forwardWebSocket';

export { MyDurableObject } from './MyDurableObject';
export { MyWorkflow } from './MyWorkflow';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const match = new URL(request.url).pathname.match(/^\/ws\/(.+)$/);
    if (!match) return new Response('Not found', { status: 404 });
    return forwardWebSocket(request, env.MY_DO, match[1]);
  }
};

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:

// src/routes/ws/[id]/+server.ts
import { forwardWebSocket } from '$lib/server/forwardWebSocket';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = ({ params, request, platform }) =>
  forwardWebSocket(request, platform!.env.MY_DO, params.id);

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.

Connecting from the client

Use __DEV_WORKER_PORT__ to pick between the sidecar (dev) and the SvelteKit route (production):

import { dev } from '$app/environment';

let wsUrl: string;
if (dev) {
  wsUrl = `ws://${window.location.hostname}:${__DEV_WORKER_PORT__}/ws/${id}`;
} else {
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  wsUrl = `${protocol}//${window.location.host}/ws/${id}`;
}
const ws = new WebSocket(wsUrl);

Add the type declaration to your src/app.d.ts:

declare global {
  const __DEV_WORKER_PORT__: number;
}

Importing non-JS modules (.sql, .txt, …)

Wrangler lets workers import non-JavaScript files as modules via module rules: Text rules import as a string, Data rules as an ArrayBuffer. Wrangler applies a set of default rules even without any config — **/*.txt, **/*.html, and **/*.sql as Text, **/*.bin as Data.

The plugin honors the same contract in both modes: the dev sidecar is bundled by wrangler itself, and the build-mode esbuild pass translates the rules from your wrangler config (plus wrangler's defaults) into esbuild loaders and inlines the file contents into _extra_exports.js.

This makes Drizzle's durable-sqlite migrations work out of the box (#8): the generated drizzle/migrations.js imports .sql files, which wrangler's default rules cover — no rules config needed. Custom extensions need a rule:

// wrangler.jsonc
"rules": [{ "type": "Text", "globs": ["**/*.graphql"], "fallthrough": true }]

And a type declaration so TypeScript accepts the import:

// src/app.d.ts
declare module '*.graphql' {
	const contents: string;
	export default contents;
}

Caveats:

  • CompiledWasm, PythonModule, and PythonRequirement rules can't be inlined into the single-file production bundle; importing a file matched by one fails the build with an explanatory error.
  • Glob matching in build mode uses miniflare's matcher (globstar semantics, matched against resolved file paths) while wrangler's dev bundler matches import specifiers with non-globstar semantics. The canonical shapes (**/*.ext) behave identically under both; exotic globs may differ.

Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | entryPoint | string | Yes | -- | Path to the file that exports your DO/Workflow classes | | outputDir | string | No | .svelte-kit/cloudflare | Directory containing the SvelteKit-generated _worker.js | | wranglerConfig | string | No | auto-discovered | Path to wrangler config file | | devPort | number | No | 8787 | Port for the dev worker server |

How it works

Build mode

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:

  1. Bundles your entryPoint with esbuild into _extra_exports.js, applying the wrangler config's module rules (plus wrangler's defaults) so .sql/.txt-style imports are inlined — see Importing non-JS modules
  2. Renames the original _worker.js to _sveltekit_worker.js
  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:
import sveltekitWorker from './_sveltekit_worker.js';
import * as extra from './_extra_exports.js';
export * from './_extra_exports.js';
const { fetch: _ignored, ...extraHandlers } = extra.default ?? {};
export default { ...sveltekitWorker, ...extraHandlers };
  1. 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.

The operation is idempotent -- if _sveltekit_worker.js already exists, the second hook skips. Tested against SvelteKit v2 (see example/) and v3 (see example-v3/).

Dev mode

The plugin reads your wrangler config, creates a temporary config with main pointing to your entryPoint, and starts a wrangler dev server via unstable_startWorker. This gives you:

  • Real workerd runtime (not emulated)
  • Hot-reload when you change DO code
  • Same WebSocket protocol as production
  • Fully typed Durable Object bindings (see below)

Generating typed bindings

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:

wrangler types --config .dev-worker-wrangler.jsonc

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).

Add this to your package.json scripts for convenience:

{
  "scripts": {
    "types": "wrangler types --config .dev-worker-wrangler.jsonc"
  }
}

Note: the .dev-worker-wrangler.jsonc file is generated when the dev server starts. Run pnpm dev at least once before running wrangler types.

Why this exists

SvelteKit's adapter-cloudflare does not support named exports from the worker entry point (sveltejs/kit#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.

When this plugin can be retired

This plugin is a stopgap. The single blocker for retirement is sveltejs/kit#15627 — which replaces adapter-cloudflare's custom build with @cloudflare/vite-plugin. Open as of writing. Closes #1712, #10496, #13692, #2963, #13300, #1519.

Once that PR lands, @cloudflare/vite-plugin handles everything this plugin does, natively:

  • 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.
  • 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.
  • Types: wrangler types resolves typed bindings (DurableObjectNamespace<MyDO>) from the user entry, no .dev-worker-wrangler.jsonc indirection.

Side note: cloudflare/workers-sdk#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) 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.

License

MIT