@oselvar/sveltekit-add-worker-exports 2.1.0 → 2.1.2
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 +50 -3
- package/dist/index.d.ts +42 -0
- package/dist/index.js +148 -53
- package/dist/moduleRules.d.ts +46 -0
- package/dist/moduleRules.js +130 -0
- package/package.json +20 -11
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).
|
|
@@ -215,6 +217,49 @@ declare global {
|
|
|
215
217
|
}
|
|
216
218
|
```
|
|
217
219
|
|
|
220
|
+
## Importing non-JS modules (`.sql`, `.txt`, …)
|
|
221
|
+
|
|
222
|
+
Wrangler lets workers import non-JavaScript files as modules via
|
|
223
|
+
[module rules](https://developers.cloudflare.com/workers/wrangler/configuration/#bundling):
|
|
224
|
+
`Text` rules import as a `string`, `Data` rules as an `ArrayBuffer`. Wrangler
|
|
225
|
+
applies a set of default rules even without any config — `**/*.txt`,
|
|
226
|
+
`**/*.html`, and `**/*.sql` as `Text`, `**/*.bin` as `Data`.
|
|
227
|
+
|
|
228
|
+
The plugin honors the same contract in both modes: the dev sidecar is bundled
|
|
229
|
+
by wrangler itself, and the build-mode esbuild pass translates the `rules`
|
|
230
|
+
from your wrangler config (plus wrangler's defaults) into esbuild loaders and
|
|
231
|
+
inlines the file contents into `_extra_exports.js`.
|
|
232
|
+
|
|
233
|
+
This makes [Drizzle's durable-sqlite migrations](https://orm.drizzle.team/docs/sqlite/connect-cloudflare-do)
|
|
234
|
+
work out of the box ([#8](https://github.com/oselvar/sveltekit-add-worker-exports/issues/8)):
|
|
235
|
+
the generated `drizzle/migrations.js` imports `.sql` files, which wrangler's
|
|
236
|
+
default rules cover — no `rules` config needed. Custom extensions need a rule:
|
|
237
|
+
|
|
238
|
+
```jsonc
|
|
239
|
+
// wrangler.jsonc
|
|
240
|
+
"rules": [{ "type": "Text", "globs": ["**/*.graphql"], "fallthrough": true }]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
And a type declaration so TypeScript accepts the import:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// src/app.d.ts
|
|
247
|
+
declare module '*.graphql' {
|
|
248
|
+
const contents: string;
|
|
249
|
+
export default contents;
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Caveats:
|
|
254
|
+
|
|
255
|
+
- `CompiledWasm`, `PythonModule`, and `PythonRequirement` rules can't be
|
|
256
|
+
inlined into the single-file production bundle; importing a file matched by
|
|
257
|
+
one fails the build with an explanatory error.
|
|
258
|
+
- Glob matching in build mode uses miniflare's matcher (`globstar` semantics,
|
|
259
|
+
matched against resolved file paths) while wrangler's dev bundler matches
|
|
260
|
+
import specifiers with non-`globstar` semantics. The canonical shapes
|
|
261
|
+
(`**/*.ext`) behave identically under both; exotic globs may differ.
|
|
262
|
+
|
|
218
263
|
## Options
|
|
219
264
|
|
|
220
265
|
| Option | Type | Required | Default | Description |
|
|
@@ -228,9 +273,9 @@ declare global {
|
|
|
228
273
|
|
|
229
274
|
### Build mode
|
|
230
275
|
|
|
231
|
-
The plugin runs
|
|
276
|
+
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
277
|
|
|
233
|
-
1. Bundles your `entryPoint` with esbuild into `_extra_exports.js`
|
|
278
|
+
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](#importing-non-js-modules-sql-txt-)
|
|
234
279
|
2. Renames the original `_worker.js` to `_sveltekit_worker.js`
|
|
235
280
|
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:
|
|
236
281
|
|
|
@@ -242,7 +287,9 @@ const { fetch: _ignored, ...extraHandlers } = extra.default ?? {};
|
|
|
242
287
|
export default { ...sveltekitWorker, ...extraHandlers };
|
|
243
288
|
```
|
|
244
289
|
|
|
245
|
-
|
|
290
|
+
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.
|
|
291
|
+
|
|
292
|
+
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
293
|
|
|
247
294
|
### Dev mode
|
|
248
295
|
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
*
|
|
21
21
|
* @see https://github.com/sveltejs/kit/issues/1712
|
|
22
22
|
*/
|
|
23
|
+
import { type ModuleRule } from 'miniflare';
|
|
23
24
|
import type { Plugin } from 'vite';
|
|
25
|
+
export { createModuleRulesPlugin, toModuleRule, WRANGLER_DEFAULT_MODULE_RULES, type WranglerRule } from './moduleRules.js';
|
|
24
26
|
/**
|
|
25
27
|
* Parses a wrangler config file. Picks the parser by extension:
|
|
26
28
|
* `.toml` uses smol-toml; everything else (`.jsonc`, `.json`, or no
|
|
@@ -29,6 +31,23 @@ import type { Plugin } from 'vite';
|
|
|
29
31
|
* Exported for tests; the dev plugin uses it via the configureServer hook.
|
|
30
32
|
*/
|
|
31
33
|
export declare function parseWranglerConfig(path: string, contents: string): unknown;
|
|
34
|
+
/**
|
|
35
|
+
* The build outputs this plugin writes into the adapter's output dir. They are
|
|
36
|
+
* server-side / Durable Object bundles and their source maps — never meant to
|
|
37
|
+
* be downloadable. adapter-cloudflare's generated `.assetsignore` only excludes
|
|
38
|
+
* its own outputs (`_worker.js`, `_routes.json`, `_headers`, `_redirects`), so
|
|
39
|
+
* without this Cloudflare would serve these as public static assets, leaking
|
|
40
|
+
* the bundled code and (via the maps' `sourcesContent`) the original
|
|
41
|
+
* TypeScript. See https://github.com/oselvar/sveltekit-add-worker-exports/issues/4
|
|
42
|
+
*/
|
|
43
|
+
export declare const PLUGIN_ASSETS_IGNORE_ENTRIES: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Returns `.assetsignore` content with `entries` appended, skipping any that
|
|
46
|
+
* are already present (trim-compared). Preserves existing lines and order,
|
|
47
|
+
* normalises to a trailing newline. Idempotent — `.assetsignore` controls only
|
|
48
|
+
* public asset serving, so the listed files stay on disk for wrangler to bundle.
|
|
49
|
+
*/
|
|
50
|
+
export declare function mergeAssetsIgnore(existing: string, entries: string[]): string;
|
|
32
51
|
export interface AddWorkerExportsOptions {
|
|
33
52
|
/** File that exports Durable Object and/or Workflow classes */
|
|
34
53
|
entryPoint: string;
|
|
@@ -65,3 +84,26 @@ export interface AddWorkerExportsOptions {
|
|
|
65
84
|
* ```
|
|
66
85
|
*/
|
|
67
86
|
export declare function addWorkerExports(options: AddWorkerExportsOptions): Plugin[];
|
|
87
|
+
/**
|
|
88
|
+
* Patches the adapter-generated `_worker.js`: bundles the named exports into
|
|
89
|
+
* `_extra_exports.js`, renames the original worker to `_sveltekit_worker.js`,
|
|
90
|
+
* and writes a merged `_worker.js`. Idempotent — safe to call from more than
|
|
91
|
+
* one Vite hook; the first call that finds `_worker.js` does the work, later
|
|
92
|
+
* calls see `_sveltekit_worker.js` and return.
|
|
93
|
+
*
|
|
94
|
+
* Returns `true` if it patched (or had already patched), `false` if the
|
|
95
|
+
* adapter hasn't written `_worker.js` yet.
|
|
96
|
+
*/
|
|
97
|
+
/**
|
|
98
|
+
* Reads the module `rules` from the wrangler config (auto-discovered, or
|
|
99
|
+
* `options.wranglerConfig` when set) for the build-mode bundle. Wrangler
|
|
100
|
+
* resolves `CLOUDFLARE_ENV` and per-env `rules` inheritance itself.
|
|
101
|
+
*
|
|
102
|
+
* When no config is found by auto-discovery, wrangler returns an empty
|
|
103
|
+
* config, so this yields no user rules (wrangler's default rules still
|
|
104
|
+
* apply). A missing `wranglerConfig` path, a syntax error, or a validation
|
|
105
|
+
* error throws and fails the build — the same config would fail
|
|
106
|
+
* `wrangler dev`/`deploy`, so surfacing it here is strictly earlier, not a
|
|
107
|
+
* new requirement.
|
|
108
|
+
*/
|
|
109
|
+
export declare function readModuleRules(options: AddWorkerExportsOptions): Promise<ModuleRule[]>;
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,8 @@ import { join, resolve } from 'node:path';
|
|
|
29
29
|
import { parse as parseJsonc } from 'jsonc-parser';
|
|
30
30
|
import { getNodeCompat } from 'miniflare';
|
|
31
31
|
import { parse as parseToml } from 'smol-toml';
|
|
32
|
+
import { createModuleRulesPlugin, toModuleRule } from './moduleRules.js';
|
|
33
|
+
export { createModuleRulesPlugin, toModuleRule, WRANGLER_DEFAULT_MODULE_RULES } from './moduleRules.js';
|
|
32
34
|
/**
|
|
33
35
|
* Parses a wrangler config file. Picks the parser by extension:
|
|
34
36
|
* `.toml` uses smol-toml; everything else (`.jsonc`, `.json`, or no
|
|
@@ -40,6 +42,37 @@ export function parseWranglerConfig(path, contents) {
|
|
|
40
42
|
return path.endsWith('.toml') ? parseToml(contents) : parseJsonc(contents);
|
|
41
43
|
}
|
|
42
44
|
const DEFAULT_DEV_PORT = 8787;
|
|
45
|
+
/**
|
|
46
|
+
* The build outputs this plugin writes into the adapter's output dir. They are
|
|
47
|
+
* server-side / Durable Object bundles and their source maps — never meant to
|
|
48
|
+
* be downloadable. adapter-cloudflare's generated `.assetsignore` only excludes
|
|
49
|
+
* its own outputs (`_worker.js`, `_routes.json`, `_headers`, `_redirects`), so
|
|
50
|
+
* without this Cloudflare would serve these as public static assets, leaking
|
|
51
|
+
* the bundled code and (via the maps' `sourcesContent`) the original
|
|
52
|
+
* TypeScript. See https://github.com/oselvar/sveltekit-add-worker-exports/issues/4
|
|
53
|
+
*/
|
|
54
|
+
export const PLUGIN_ASSETS_IGNORE_ENTRIES = [
|
|
55
|
+
'_sveltekit_worker.js',
|
|
56
|
+
'_sveltekit_worker.js.map',
|
|
57
|
+
'_extra_exports.js',
|
|
58
|
+
'_extra_exports.js.map'
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Returns `.assetsignore` content with `entries` appended, skipping any that
|
|
62
|
+
* are already present (trim-compared). Preserves existing lines and order,
|
|
63
|
+
* normalises to a trailing newline. Idempotent — `.assetsignore` controls only
|
|
64
|
+
* public asset serving, so the listed files stay on disk for wrangler to bundle.
|
|
65
|
+
*/
|
|
66
|
+
export function mergeAssetsIgnore(existing, entries) {
|
|
67
|
+
const lines = existing.split('\n');
|
|
68
|
+
const present = new Set(lines.map((l) => l.trim()));
|
|
69
|
+
const additions = entries.filter((e) => !present.has(e.trim()));
|
|
70
|
+
// Drop trailing blank lines so we append cleanly, then re-add one newline.
|
|
71
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
72
|
+
lines.pop();
|
|
73
|
+
}
|
|
74
|
+
return [...lines, ...additions].join('\n') + '\n';
|
|
75
|
+
}
|
|
43
76
|
/**
|
|
44
77
|
* Mirrors wrangler's internal getRegistryPath() — wrangler doesn't export it.
|
|
45
78
|
* Workers register themselves under this path so cross-worker `script_name`
|
|
@@ -76,66 +109,126 @@ function getWranglerRegistryPath() {
|
|
|
76
109
|
export function addWorkerExports(options) {
|
|
77
110
|
return [buildPlugin(options), devPlugin(options)];
|
|
78
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Patches the adapter-generated `_worker.js`: bundles the named exports into
|
|
114
|
+
* `_extra_exports.js`, renames the original worker to `_sveltekit_worker.js`,
|
|
115
|
+
* and writes a merged `_worker.js`. Idempotent — safe to call from more than
|
|
116
|
+
* one Vite hook; the first call that finds `_worker.js` does the work, later
|
|
117
|
+
* calls see `_sveltekit_worker.js` and return.
|
|
118
|
+
*
|
|
119
|
+
* Returns `true` if it patched (or had already patched), `false` if the
|
|
120
|
+
* adapter hasn't written `_worker.js` yet.
|
|
121
|
+
*/
|
|
122
|
+
/**
|
|
123
|
+
* Reads the module `rules` from the wrangler config (auto-discovered, or
|
|
124
|
+
* `options.wranglerConfig` when set) for the build-mode bundle. Wrangler
|
|
125
|
+
* resolves `CLOUDFLARE_ENV` and per-env `rules` inheritance itself.
|
|
126
|
+
*
|
|
127
|
+
* When no config is found by auto-discovery, wrangler returns an empty
|
|
128
|
+
* config, so this yields no user rules (wrangler's default rules still
|
|
129
|
+
* apply). A missing `wranglerConfig` path, a syntax error, or a validation
|
|
130
|
+
* error throws and fails the build — the same config would fail
|
|
131
|
+
* `wrangler dev`/`deploy`, so surfacing it here is strictly earlier, not a
|
|
132
|
+
* new requirement.
|
|
133
|
+
*/
|
|
134
|
+
export async function readModuleRules(options) {
|
|
135
|
+
// Dynamic import: loading wrangler's bundle takes ~500ms, and this module
|
|
136
|
+
// is evaluated on every vite invocation via vite.config.ts. Deferring the
|
|
137
|
+
// import to the once-per-run call sites (here and the dev plugin's
|
|
138
|
+
// configureServer) keeps that cost out of vite's config load.
|
|
139
|
+
const { unstable_readConfig } = await import('wrangler');
|
|
140
|
+
const config = unstable_readConfig(options.wranglerConfig ? { config: options.wranglerConfig } : {}, { hideWarnings: true });
|
|
141
|
+
return (config.rules ?? []).map(toModuleRule);
|
|
142
|
+
}
|
|
143
|
+
async function patchWorker(options) {
|
|
144
|
+
const outputDir = resolve(options.outputDir ?? '.svelte-kit/cloudflare');
|
|
145
|
+
const workerPath = resolve(outputDir, '_worker.js');
|
|
146
|
+
const sveltekitPath = resolve(outputDir, '_sveltekit_worker.js');
|
|
147
|
+
const exportsPath = resolve(outputDir, '_extra_exports.js');
|
|
148
|
+
// Skip if already patched (idempotent)
|
|
149
|
+
if (await exists(sveltekitPath)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
// Skip if the adapter hasn't run yet (e.g. during the SSR build phase, or
|
|
153
|
+
// before SvelteKit v3's `buildApp` hook runs the adapter)
|
|
154
|
+
if (!(await exists(workerPath))) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
// Bundle the named exports.
|
|
158
|
+
//
|
|
159
|
+
// `conditions: ['workerd', 'worker', 'browser']` is the Cloudflare-
|
|
160
|
+
// recommended set for bundlers targeting Workers — it picks the
|
|
161
|
+
// workerd-specific or browser-shimmed variants of packages over
|
|
162
|
+
// their Node variants (e.g. nanoid's webcrypto-via-`globalThis.crypto`
|
|
163
|
+
// browser build instead of the `node:crypto`-importing main build).
|
|
164
|
+
//
|
|
165
|
+
// Node built-ins are externalised so transitive deps that do
|
|
166
|
+
// `require('path')` or `import 'node:async_hooks'` survive bundling;
|
|
167
|
+
// the Workers runtime resolves them when `nodejs_compat` is enabled
|
|
168
|
+
// in wrangler.jsonc.
|
|
169
|
+
//
|
|
170
|
+
// The module-rules plugin applies the wrangler config's `rules` (plus
|
|
171
|
+
// wrangler's defaults, e.g. `.sql`/`.txt` as Text), matching what
|
|
172
|
+
// wrangler's own bundler does for the dev sidecar. See moduleRules.ts.
|
|
173
|
+
await build({
|
|
174
|
+
entryPoints: [options.entryPoint],
|
|
175
|
+
bundle: true,
|
|
176
|
+
format: 'esm',
|
|
177
|
+
sourcemap: true,
|
|
178
|
+
target: 'esnext',
|
|
179
|
+
conditions: ['workerd', 'worker', 'browser'],
|
|
180
|
+
external: ['cloudflare:*', ...NODE_BUILTINS, ...NODE_BUILTINS.map((m) => `node:${m}`)],
|
|
181
|
+
plugins: [createModuleRulesPlugin(await readModuleRules(options))],
|
|
182
|
+
outfile: exportsPath
|
|
183
|
+
});
|
|
184
|
+
// Rename original worker and create merged entry point.
|
|
185
|
+
//
|
|
186
|
+
// `export *` re-exports named exports only, so class-based bindings
|
|
187
|
+
// (DOs, Workflows, WorkerEntrypoint) from `_extra_exports.js` flow
|
|
188
|
+
// through. Non-fetch handlers (scheduled, queue, email, tail, trace)
|
|
189
|
+
// live on the user entry's `default` export — Cloudflare invokes
|
|
190
|
+
// them as methods on the worker's default object, not as named
|
|
191
|
+
// exports — so we spread them onto the SvelteKit default. `fetch`
|
|
192
|
+
// is dropped from the user default because SvelteKit owns request
|
|
193
|
+
// handling in production.
|
|
194
|
+
await rename(workerPath, sveltekitPath);
|
|
195
|
+
await writeFile(workerPath, `import sveltekitWorker from './_sveltekit_worker.js';\n` +
|
|
196
|
+
`import * as extra from './_extra_exports.js';\n` +
|
|
197
|
+
`export * from './_extra_exports.js';\n` +
|
|
198
|
+
`const { fetch: _ignored, ...extraHandlers } = extra.default ?? {};\n` +
|
|
199
|
+
`export default { ...sveltekitWorker, ...extraHandlers };\n`);
|
|
200
|
+
// Keep this plugin's server bundles and source maps out of the
|
|
201
|
+
// publicly-served assets. The adapter regenerates `.assetsignore` every
|
|
202
|
+
// build with only its own outputs; we append ours here, right after
|
|
203
|
+
// patching, so it survives each deploy regardless of which hook ran the
|
|
204
|
+
// patch (v2: `closeBundle`, v3: `buildApp`).
|
|
205
|
+
const assetsIgnorePath = resolve(outputDir, '.assetsignore');
|
|
206
|
+
const currentIgnore = (await exists(assetsIgnorePath))
|
|
207
|
+
? await readFile(assetsIgnorePath, 'utf-8')
|
|
208
|
+
: '';
|
|
209
|
+
await writeFile(assetsIgnorePath, mergeAssetsIgnore(currentIgnore, PLUGIN_ASSETS_IGNORE_ENTRIES));
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
79
212
|
function buildPlugin(options) {
|
|
80
213
|
return {
|
|
81
214
|
name: 'add-worker-exports',
|
|
82
215
|
apply: 'build',
|
|
83
216
|
enforce: 'post',
|
|
217
|
+
// SvelteKit v2 runs the Cloudflare adapter inside `closeBundle`; our
|
|
218
|
+
// `enforce: 'post'` hook then runs after it and sees `_worker.js`.
|
|
84
219
|
async closeBundle() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
220
|
+
await patchWorker(options);
|
|
221
|
+
},
|
|
222
|
+
// SvelteKit v3 moved the adapter into Vite's `buildApp` hook, which runs
|
|
223
|
+
// *after* every `closeBundle`. By that point our `closeBundle` has
|
|
224
|
+
// already run and skipped (no `_worker.js` yet), so we patch here
|
|
225
|
+
// instead. `order: 'post'` ensures we run after SvelteKit's own
|
|
226
|
+
// `buildApp` that generates `_worker.js`. Harmless on v2 (idempotent).
|
|
227
|
+
buildApp: {
|
|
228
|
+
order: 'post',
|
|
229
|
+
async handler() {
|
|
230
|
+
await patchWorker(options);
|
|
96
231
|
}
|
|
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
232
|
}
|
|
140
233
|
};
|
|
141
234
|
}
|
|
@@ -145,6 +238,7 @@ function devPlugin(options) {
|
|
|
145
238
|
let tempConfigPath = null;
|
|
146
239
|
let proxyConfigPath = null;
|
|
147
240
|
async function readRawConfig() {
|
|
241
|
+
// Dynamic import — see readModuleRules for why.
|
|
148
242
|
const { unstable_readConfig } = await import('wrangler');
|
|
149
243
|
const parsed = unstable_readConfig(options.wranglerConfig ? { config: options.wranglerConfig } : {});
|
|
150
244
|
const path = parsed.configPath;
|
|
@@ -162,6 +256,7 @@ function devPlugin(options) {
|
|
|
162
256
|
};
|
|
163
257
|
},
|
|
164
258
|
async configureServer(server) {
|
|
259
|
+
// Dynamic import — see readModuleRules for why.
|
|
165
260
|
const { unstable_startWorker } = await import('wrangler');
|
|
166
261
|
const { path: configPath, contents } = await readRawConfig();
|
|
167
262
|
const baseConfig = parseWranglerConfig(configPath, contents);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates wrangler module `rules` into an esbuild plugin so the build-mode
|
|
3
|
+
* bundle honors the same contract wrangler's own bundler honors in dev.
|
|
4
|
+
*
|
|
5
|
+
* Wrangler config rules look like
|
|
6
|
+
* `{ "type": "Text", "globs": ["**\/*.sql"], "fallthrough": true }` and make
|
|
7
|
+
* matching imports resolve to the file contents (as a string for `Text`, an
|
|
8
|
+
* `ArrayBuffer` for `Data`). Wrangler applies them — plus a set of default
|
|
9
|
+
* rules — whenever it bundles a worker (`wrangler dev`, `wrangler deploy`,
|
|
10
|
+
* and this plugin's dev sidecar). The build-mode `esbuild.build()` call in
|
|
11
|
+
* `patchWorker` runs outside wrangler, so without this plugin a `.sql` import
|
|
12
|
+
* that works in dev fails the production build with "No loader is configured".
|
|
13
|
+
* See https://github.com/oselvar/sveltekit-add-worker-exports/issues/8
|
|
14
|
+
*
|
|
15
|
+
* Wrangler exports none of its rules machinery, but miniflare (already a peer
|
|
16
|
+
* dependency) exports `compileModuleRules`/`testRegExps` with the same
|
|
17
|
+
* fallthrough semantics, so only wrangler's default rule list is replicated
|
|
18
|
+
* here. One divergence: miniflare compiles globs with `globstar: true` and
|
|
19
|
+
* matches resolved file paths, while wrangler's bundler compiles with
|
|
20
|
+
* `globstar: false` and matches import specifiers. The canonical shapes
|
|
21
|
+
* (`**\/*.sql`, `**\/*.txt`) behave identically under both.
|
|
22
|
+
*/
|
|
23
|
+
import type { Plugin as EsbuildPlugin } from 'esbuild';
|
|
24
|
+
import { type ModuleRule, type ModuleRuleType } from 'miniflare';
|
|
25
|
+
/** A module rule as it appears in wrangler config (miniflare uses `include`). */
|
|
26
|
+
export interface WranglerRule {
|
|
27
|
+
type: ModuleRuleType;
|
|
28
|
+
globs: string[];
|
|
29
|
+
fallthrough?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function toModuleRule(rule: WranglerRule): ModuleRule;
|
|
32
|
+
/**
|
|
33
|
+
* Wrangler's `DEFAULT_MODULE_RULES` (src/deployment-bundle/rules.ts), in
|
|
34
|
+
* miniflare's shape. Wrangler appends these after user rules everywhere it
|
|
35
|
+
* bundles, so the plugin does the same — `.sql`, `.txt`, and `.html` imports
|
|
36
|
+
* work with no `rules` config at all, matching `wrangler dev`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const WRANGLER_DEFAULT_MODULE_RULES: ModuleRule[];
|
|
39
|
+
/**
|
|
40
|
+
* Builds an esbuild plugin that applies `userRules` followed by wrangler's
|
|
41
|
+
* default rules, first match wins. Matching honors wrangler's fallthrough
|
|
42
|
+
* semantics: the first rule of a type without `fallthrough: true` finalises
|
|
43
|
+
* that type, and a file matching only a later (shadowed) rule of that type
|
|
44
|
+
* fails the build with wrangler's own actionable error.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createModuleRulesPlugin(userRules: ModuleRule[]): EsbuildPlugin;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates wrangler module `rules` into an esbuild plugin so the build-mode
|
|
3
|
+
* bundle honors the same contract wrangler's own bundler honors in dev.
|
|
4
|
+
*
|
|
5
|
+
* Wrangler config rules look like
|
|
6
|
+
* `{ "type": "Text", "globs": ["**\/*.sql"], "fallthrough": true }` and make
|
|
7
|
+
* matching imports resolve to the file contents (as a string for `Text`, an
|
|
8
|
+
* `ArrayBuffer` for `Data`). Wrangler applies them — plus a set of default
|
|
9
|
+
* rules — whenever it bundles a worker (`wrangler dev`, `wrangler deploy`,
|
|
10
|
+
* and this plugin's dev sidecar). The build-mode `esbuild.build()` call in
|
|
11
|
+
* `patchWorker` runs outside wrangler, so without this plugin a `.sql` import
|
|
12
|
+
* that works in dev fails the production build with "No loader is configured".
|
|
13
|
+
* See https://github.com/oselvar/sveltekit-add-worker-exports/issues/8
|
|
14
|
+
*
|
|
15
|
+
* Wrangler exports none of its rules machinery, but miniflare (already a peer
|
|
16
|
+
* dependency) exports `compileModuleRules`/`testRegExps` with the same
|
|
17
|
+
* fallthrough semantics, so only wrangler's default rule list is replicated
|
|
18
|
+
* here. One divergence: miniflare compiles globs with `globstar: true` and
|
|
19
|
+
* matches resolved file paths, while wrangler's bundler compiles with
|
|
20
|
+
* `globstar: false` and matches import specifiers. The canonical shapes
|
|
21
|
+
* (`**\/*.sql`, `**\/*.txt`) behave identically under both.
|
|
22
|
+
*/
|
|
23
|
+
import { readFile } from 'node:fs/promises';
|
|
24
|
+
import { compileModuleRules, globsToRegExps, testRegExps } from 'miniflare';
|
|
25
|
+
export function toModuleRule(rule) {
|
|
26
|
+
return { type: rule.type, include: rule.globs, fallthrough: rule.fallthrough };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wrangler's `DEFAULT_MODULE_RULES` (src/deployment-bundle/rules.ts), in
|
|
30
|
+
* miniflare's shape. Wrangler appends these after user rules everywhere it
|
|
31
|
+
* bundles, so the plugin does the same — `.sql`, `.txt`, and `.html` imports
|
|
32
|
+
* work with no `rules` config at all, matching `wrangler dev`.
|
|
33
|
+
*/
|
|
34
|
+
export const WRANGLER_DEFAULT_MODULE_RULES = [
|
|
35
|
+
{ type: 'Text', include: ['**/*.txt', '**/*.html', '**/*.sql'] },
|
|
36
|
+
{ type: 'Data', include: ['**/*.bin'] },
|
|
37
|
+
{ type: 'CompiledWasm', include: ['**/*.wasm', '**/*.wasm?module'] }
|
|
38
|
+
];
|
|
39
|
+
/** Rules esbuild already handles natively; wrangler skips these too. */
|
|
40
|
+
function isJavaScriptRuleType(type) {
|
|
41
|
+
return type === 'ESModule' || type === 'CommonJS';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds an esbuild plugin that applies `userRules` followed by wrangler's
|
|
45
|
+
* default rules, first match wins. Matching honors wrangler's fallthrough
|
|
46
|
+
* semantics: the first rule of a type without `fallthrough: true` finalises
|
|
47
|
+
* that type, and a file matching only a later (shadowed) rule of that type
|
|
48
|
+
* fails the build with wrangler's own actionable error.
|
|
49
|
+
*/
|
|
50
|
+
export function createModuleRulesPlugin(userRules) {
|
|
51
|
+
const allRules = [...userRules, ...WRANGLER_DEFAULT_MODULE_RULES];
|
|
52
|
+
// `compileModuleRules` keeps the same rules wrangler's `parseRules` keeps.
|
|
53
|
+
// JS rules are dropped from the match loop — esbuild's default handling is
|
|
54
|
+
// exactly their meaning — but they still participate in fallthrough
|
|
55
|
+
// finalisation above, like in wrangler.
|
|
56
|
+
const kept = compileModuleRules(allRules).filter((rule) => !isJavaScriptRuleType(rule.type));
|
|
57
|
+
// Recompute which rules `compileModuleRules` dropped, so files matching
|
|
58
|
+
// only a shadowed rule get wrangler's error instead of esbuild's opaque
|
|
59
|
+
// "No loader is configured".
|
|
60
|
+
const finalisedTypes = new Set();
|
|
61
|
+
const removed = [];
|
|
62
|
+
for (const rule of allRules) {
|
|
63
|
+
if (finalisedTypes.has(rule.type)) {
|
|
64
|
+
removed.push({ rule, include: globsToRegExps(rule.include, { endAnchor: true }) });
|
|
65
|
+
}
|
|
66
|
+
else if (!rule.fallthrough) {
|
|
67
|
+
finalisedTypes.add(rule.type);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name: 'wrangler-module-rules',
|
|
72
|
+
setup(build) {
|
|
73
|
+
build.onLoad({ filter: /.*/ }, async (args) => {
|
|
74
|
+
// Match with `/` separators so globs behave the same on Windows.
|
|
75
|
+
const path = args.path.replaceAll('\\', '/');
|
|
76
|
+
for (const rule of kept) {
|
|
77
|
+
if (testRegExps(rule.include, path)) {
|
|
78
|
+
return loadModule(rule.type, args.path);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const { rule, include } of removed) {
|
|
82
|
+
if (testRegExps(include, path)) {
|
|
83
|
+
return {
|
|
84
|
+
errors: [
|
|
85
|
+
{
|
|
86
|
+
text: `The file ${args.path} matched a module rule in your configuration ` +
|
|
87
|
+
`(${JSON.stringify({ type: rule.type, globs: rule.include })}), but was ignored because a ` +
|
|
88
|
+
'previous rule with the same type was not marked as `fallthrough = true`.'
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function loadModule(type, path) {
|
|
100
|
+
switch (type) {
|
|
101
|
+
case 'Text':
|
|
102
|
+
// esbuild's text loader default-exports the contents as a string,
|
|
103
|
+
// exactly like a workerd Text module.
|
|
104
|
+
return { contents: await readFile(path, 'utf-8'), loader: 'text' };
|
|
105
|
+
case 'Data': {
|
|
106
|
+
// workerd Data modules import as an ArrayBuffer, but esbuild's
|
|
107
|
+
// `binary` loader yields a Uint8Array — so inline a stub that
|
|
108
|
+
// decodes to the exact runtime type.
|
|
109
|
+
const base64 = (await readFile(path)).toString('base64');
|
|
110
|
+
return {
|
|
111
|
+
contents: `const bytes = Uint8Array.from(atob(${JSON.stringify(base64)}), (c) => c.charCodeAt(0));\n` +
|
|
112
|
+
'export default bytes.buffer;\n',
|
|
113
|
+
loader: 'js'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
default:
|
|
117
|
+
// CompiledWasm / PythonModule / PythonRequirement: workerd loads
|
|
118
|
+
// these as separate deploy-time modules, which can't be inlined
|
|
119
|
+
// into the single-file _extra_exports.js bundle. (Workers forbids
|
|
120
|
+
// compiling WebAssembly from bytes at runtime.)
|
|
121
|
+
return {
|
|
122
|
+
errors: [
|
|
123
|
+
{
|
|
124
|
+
text: `"${type}" module rules are not supported by @oselvar/sveltekit-add-worker-exports: ` +
|
|
125
|
+
`${path} cannot be inlined into the single-file worker bundle.`
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oselvar/sveltekit-add-worker-exports",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Vite plugin that adds Durable Object and Workflow exports to SvelteKit Cloudflare workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -37,24 +37,33 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@release-it/conventional-changelog": "^11.0.1",
|
|
40
|
-
"@sveltejs/kit": "^2.
|
|
41
|
-
"@types/node": "^
|
|
42
|
-
"esbuild": "^0.28.
|
|
43
|
-
"miniflare": "^4.
|
|
44
|
-
"release-it": "^20.2.
|
|
45
|
-
"typescript": "^
|
|
46
|
-
"vite": "^8.
|
|
47
|
-
"vitest": "^4.1.
|
|
48
|
-
"wrangler": "^4.
|
|
40
|
+
"@sveltejs/kit": "^2.69.3",
|
|
41
|
+
"@types/node": "^26.1.1",
|
|
42
|
+
"esbuild": "^0.28.1",
|
|
43
|
+
"miniflare": "^4.20260710.0",
|
|
44
|
+
"release-it": "^20.2.1",
|
|
45
|
+
"typescript": "^7.0.2",
|
|
46
|
+
"vite": "^8.1.4",
|
|
47
|
+
"vitest": "^4.1.10",
|
|
48
|
+
"wrangler": "^4.111.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "tsc",
|
|
52
52
|
"test": "vitest run && tsc --noEmit",
|
|
53
|
+
"test:acceptance": "pnpm build && vitest run --config vitest.acceptance.config.ts",
|
|
53
54
|
"prepublishOnly": "tsc",
|
|
54
55
|
"release": "release-it"
|
|
55
56
|
},
|
|
57
|
+
"packageManager": "pnpm@11.9.0",
|
|
58
|
+
"devEngines": {
|
|
59
|
+
"runtime": {
|
|
60
|
+
"name": "node",
|
|
61
|
+
"version": ">=26",
|
|
62
|
+
"onFail": "warn"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
56
65
|
"dependencies": {
|
|
57
66
|
"jsonc-parser": "^3.3.1",
|
|
58
|
-
"smol-toml": "^1.
|
|
67
|
+
"smol-toml": "^1.7.0"
|
|
59
68
|
}
|
|
60
69
|
}
|