@oselvar/sveltekit-add-worker-exports 2.1.1 → 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 +55 -6
- package/bin/sveltekit-add-worker-exports.js +4 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +9 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +86 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +243 -103
- package/dist/moduleRules.d.ts +46 -0
- package/dist/moduleRules.js +130 -0
- package/package.json +24 -11
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).
|
|
@@ -217,6 +219,49 @@ declare global {
|
|
|
217
219
|
}
|
|
218
220
|
```
|
|
219
221
|
|
|
222
|
+
## Importing non-JS modules (`.sql`, `.txt`, …)
|
|
223
|
+
|
|
224
|
+
Wrangler lets workers import non-JavaScript files as modules via
|
|
225
|
+
[module rules](https://developers.cloudflare.com/workers/wrangler/configuration/#bundling):
|
|
226
|
+
`Text` rules import as a `string`, `Data` rules as an `ArrayBuffer`. Wrangler
|
|
227
|
+
applies a set of default rules even without any config — `**/*.txt`,
|
|
228
|
+
`**/*.html`, and `**/*.sql` as `Text`, `**/*.bin` as `Data`.
|
|
229
|
+
|
|
230
|
+
The plugin honors the same contract in both modes: the dev sidecar is bundled
|
|
231
|
+
by wrangler itself, and the build-mode esbuild pass translates the `rules`
|
|
232
|
+
from your wrangler config (plus wrangler's defaults) into esbuild loaders and
|
|
233
|
+
inlines the file contents into `_extra_exports.js`.
|
|
234
|
+
|
|
235
|
+
This makes [Drizzle's durable-sqlite migrations](https://orm.drizzle.team/docs/sqlite/connect-cloudflare-do)
|
|
236
|
+
work out of the box ([#8](https://github.com/oselvar/sveltekit-add-worker-exports/issues/8)):
|
|
237
|
+
the generated `drizzle/migrations.js` imports `.sql` files, which wrangler's
|
|
238
|
+
default rules cover — no `rules` config needed. Custom extensions need a rule:
|
|
239
|
+
|
|
240
|
+
```jsonc
|
|
241
|
+
// wrangler.jsonc
|
|
242
|
+
"rules": [{ "type": "Text", "globs": ["**/*.graphql"], "fallthrough": true }]
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
And a type declaration so TypeScript accepts the import:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// src/app.d.ts
|
|
249
|
+
declare module '*.graphql' {
|
|
250
|
+
const contents: string;
|
|
251
|
+
export default contents;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Caveats:
|
|
256
|
+
|
|
257
|
+
- `CompiledWasm`, `PythonModule`, and `PythonRequirement` rules can't be
|
|
258
|
+
inlined into the single-file production bundle; importing a file matched by
|
|
259
|
+
one fails the build with an explanatory error.
|
|
260
|
+
- Glob matching in build mode uses miniflare's matcher (`globstar` semantics,
|
|
261
|
+
matched against resolved file paths) while wrangler's dev bundler matches
|
|
262
|
+
import specifiers with non-`globstar` semantics. The canonical shapes
|
|
263
|
+
(`**/*.ext`) behave identically under both; exotic globs may differ.
|
|
264
|
+
|
|
220
265
|
## Options
|
|
221
266
|
|
|
222
267
|
| Option | Type | Required | Default | Description |
|
|
@@ -232,7 +277,7 @@ declare global {
|
|
|
232
277
|
|
|
233
278
|
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:
|
|
234
279
|
|
|
235
|
-
1. Bundles your `entryPoint` with esbuild into `_extra_exports.js`
|
|
280
|
+
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-)
|
|
236
281
|
2. Renames the original `_worker.js` to `_sveltekit_worker.js`
|
|
237
282
|
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:
|
|
238
283
|
|
|
@@ -259,25 +304,29 @@ The plugin reads your wrangler config, creates a temporary config with `main` po
|
|
|
259
304
|
|
|
260
305
|
### Generating typed bindings
|
|
261
306
|
|
|
262
|
-
The dev plugin
|
|
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:
|
|
263
308
|
|
|
264
309
|
```bash
|
|
310
|
+
npx sveltekit-add-worker-exports write-config
|
|
265
311
|
wrangler types --config .dev-worker-wrangler.jsonc
|
|
266
312
|
```
|
|
267
313
|
|
|
268
|
-
This produces typed DO bindings like `DurableObjectNamespace<MyDurableObject>` instead of the untyped `DurableObjectNamespace` you get from the default `wrangler.jsonc
|
|
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`).
|
|
269
315
|
|
|
270
|
-
|
|
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`:
|
|
271
317
|
|
|
272
318
|
```json
|
|
273
319
|
{
|
|
274
320
|
"scripts": {
|
|
275
|
-
"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"
|
|
276
323
|
}
|
|
277
324
|
}
|
|
278
325
|
```
|
|
279
326
|
|
|
280
|
-
|
|
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 })`.
|
|
281
330
|
|
|
282
331
|
## Why this exists
|
|
283
332
|
|
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
|
@@ -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
|
|
@@ -46,6 +48,15 @@ export declare const PLUGIN_ASSETS_IGNORE_ENTRIES: string[];
|
|
|
46
48
|
* public asset serving, so the listed files stay on disk for wrangler to bundle.
|
|
47
49
|
*/
|
|
48
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>;
|
|
49
60
|
export interface AddWorkerExportsOptions {
|
|
50
61
|
/** File that exports Durable Object and/or Workflow classes */
|
|
51
62
|
entryPoint: string;
|
|
@@ -82,3 +93,56 @@ export interface AddWorkerExportsOptions {
|
|
|
82
93
|
* ```
|
|
83
94
|
*/
|
|
84
95
|
export declare function addWorkerExports(options: AddWorkerExportsOptions): Plugin[];
|
|
96
|
+
/**
|
|
97
|
+
* Patches the adapter-generated `_worker.js`: bundles the named exports into
|
|
98
|
+
* `_extra_exports.js`, renames the original worker to `_sveltekit_worker.js`,
|
|
99
|
+
* and writes a merged `_worker.js`. Idempotent — safe to call from more than
|
|
100
|
+
* one Vite hook; the first call that finds `_worker.js` does the work, later
|
|
101
|
+
* calls see `_sveltekit_worker.js` and return.
|
|
102
|
+
*
|
|
103
|
+
* Returns `true` if it patched (or had already patched), `false` if the
|
|
104
|
+
* adapter hasn't written `_worker.js` yet.
|
|
105
|
+
*/
|
|
106
|
+
/**
|
|
107
|
+
* Reads the module `rules` from the wrangler config (auto-discovered, or
|
|
108
|
+
* `options.wranglerConfig` when set) for the build-mode bundle. Wrangler
|
|
109
|
+
* resolves `CLOUDFLARE_ENV` and per-env `rules` inheritance itself.
|
|
110
|
+
*
|
|
111
|
+
* When no config is found by auto-discovery, wrangler returns an empty
|
|
112
|
+
* config, so this yields no user rules (wrangler's default rules still
|
|
113
|
+
* apply). A missing `wranglerConfig` path, a syntax error, or a validation
|
|
114
|
+
* error throws and fails the build — the same config would fail
|
|
115
|
+
* `wrangler dev`/`deploy`, so surfacing it here is strictly earlier, not a
|
|
116
|
+
* new requirement.
|
|
117
|
+
*/
|
|
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,11 +24,14 @@ 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';
|
|
30
31
|
import { getNodeCompat } from 'miniflare';
|
|
31
32
|
import { parse as parseToml } from 'smol-toml';
|
|
33
|
+
import { createModuleRulesPlugin, toModuleRule } from './moduleRules.js';
|
|
34
|
+
export { createModuleRulesPlugin, toModuleRule, WRANGLER_DEFAULT_MODULE_RULES } from './moduleRules.js';
|
|
32
35
|
/**
|
|
33
36
|
* Parses a wrangler config file. Picks the parser by extension:
|
|
34
37
|
* `.toml` uses smol-toml; everything else (`.jsonc`, `.json`, or no
|
|
@@ -90,6 +93,42 @@ function getWranglerRegistryPath() {
|
|
|
90
93
|
}
|
|
91
94
|
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), '.wrangler', 'registry');
|
|
92
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
|
+
}
|
|
93
132
|
/**
|
|
94
133
|
* Returns two Vite plugins: one for build (patches _worker.js) and one for
|
|
95
134
|
* dev (starts a wrangler dev server for Durable Objects).
|
|
@@ -105,7 +144,7 @@ function getWranglerRegistryPath() {
|
|
|
105
144
|
* ```
|
|
106
145
|
*/
|
|
107
146
|
export function addWorkerExports(options) {
|
|
108
|
-
return [buildPlugin(options),
|
|
147
|
+
return [buildPlugin(options), ...devPlugins(options)];
|
|
109
148
|
}
|
|
110
149
|
/**
|
|
111
150
|
* Patches the adapter-generated `_worker.js`: bundles the named exports into
|
|
@@ -117,6 +156,27 @@ export function addWorkerExports(options) {
|
|
|
117
156
|
* Returns `true` if it patched (or had already patched), `false` if the
|
|
118
157
|
* adapter hasn't written `_worker.js` yet.
|
|
119
158
|
*/
|
|
159
|
+
/**
|
|
160
|
+
* Reads the module `rules` from the wrangler config (auto-discovered, or
|
|
161
|
+
* `options.wranglerConfig` when set) for the build-mode bundle. Wrangler
|
|
162
|
+
* resolves `CLOUDFLARE_ENV` and per-env `rules` inheritance itself.
|
|
163
|
+
*
|
|
164
|
+
* When no config is found by auto-discovery, wrangler returns an empty
|
|
165
|
+
* config, so this yields no user rules (wrangler's default rules still
|
|
166
|
+
* apply). A missing `wranglerConfig` path, a syntax error, or a validation
|
|
167
|
+
* error throws and fails the build — the same config would fail
|
|
168
|
+
* `wrangler dev`/`deploy`, so surfacing it here is strictly earlier, not a
|
|
169
|
+
* new requirement.
|
|
170
|
+
*/
|
|
171
|
+
export async function readModuleRules(options) {
|
|
172
|
+
// Dynamic import: loading wrangler's bundle takes ~500ms, and this module
|
|
173
|
+
// is evaluated on every vite invocation via vite.config.ts. Deferring the
|
|
174
|
+
// import to the once-per-run call sites (here and the dev plugin's
|
|
175
|
+
// configureServer) keeps that cost out of vite's config load.
|
|
176
|
+
const { unstable_readConfig } = await import('wrangler');
|
|
177
|
+
const config = unstable_readConfig(options.wranglerConfig ? { config: options.wranglerConfig } : {}, { hideWarnings: true });
|
|
178
|
+
return (config.rules ?? []).map(toModuleRule);
|
|
179
|
+
}
|
|
120
180
|
async function patchWorker(options) {
|
|
121
181
|
const outputDir = resolve(options.outputDir ?? '.svelte-kit/cloudflare');
|
|
122
182
|
const workerPath = resolve(outputDir, '_worker.js');
|
|
@@ -143,6 +203,10 @@ async function patchWorker(options) {
|
|
|
143
203
|
// `require('path')` or `import 'node:async_hooks'` survive bundling;
|
|
144
204
|
// the Workers runtime resolves them when `nodejs_compat` is enabled
|
|
145
205
|
// in wrangler.jsonc.
|
|
206
|
+
//
|
|
207
|
+
// The module-rules plugin applies the wrangler config's `rules` (plus
|
|
208
|
+
// wrangler's defaults, e.g. `.sql`/`.txt` as Text), matching what
|
|
209
|
+
// wrangler's own bundler does for the dev sidecar. See moduleRules.ts.
|
|
146
210
|
await build({
|
|
147
211
|
entryPoints: [options.entryPoint],
|
|
148
212
|
bundle: true,
|
|
@@ -151,6 +215,7 @@ async function patchWorker(options) {
|
|
|
151
215
|
target: 'esnext',
|
|
152
216
|
conditions: ['workerd', 'worker', 'browser'],
|
|
153
217
|
external: ['cloudflare:*', ...NODE_BUILTINS, ...NODE_BUILTINS.map((m) => `node:${m}`)],
|
|
218
|
+
plugins: [createModuleRulesPlugin(await readModuleRules(options))],
|
|
154
219
|
outfile: exportsPath
|
|
155
220
|
});
|
|
156
221
|
// Rename original worker and create merged entry point.
|
|
@@ -204,116 +269,191 @@ function buildPlugin(options) {
|
|
|
204
269
|
}
|
|
205
270
|
};
|
|
206
271
|
}
|
|
207
|
-
|
|
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) {
|
|
208
327
|
const devPort = options.devPort ?? DEFAULT_DEV_PORT;
|
|
209
328
|
let worker = null;
|
|
210
329
|
let tempConfigPath = null;
|
|
211
330
|
let proxyConfigPath = null;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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'));
|
|
218
380
|
}
|
|
219
|
-
return
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const { path: configPath, contents } = await readRawConfig();
|
|
232
|
-
const baseConfig = parseWranglerConfig(configPath, contents);
|
|
233
|
-
const sidecarName = `${baseConfig.name ?? 'sveltekit'}-dev-worker`;
|
|
234
|
-
// When CLOUDFLARE_ENV is set, wrangler suffixes the registered
|
|
235
|
-
// worker name with `-${env}` (see appendEnvName in wrangler), so
|
|
236
|
-
// cross-worker `script_name` lookups must use the suffixed name.
|
|
237
|
-
// We don't suffix `devConfig.name` itself — wrangler does that
|
|
238
|
-
// based on CLOUDFLARE_ENV when starting the sidecar.
|
|
239
|
-
const envName = process.env.CLOUDFLARE_ENV;
|
|
240
|
-
const registeredSidecarName = envName ? `${sidecarName}-${envName}` : sidecarName;
|
|
241
|
-
// Copy the raw config and override main, name, and dev port
|
|
242
|
-
const devConfig = structuredClone(baseConfig);
|
|
243
|
-
devConfig.name = sidecarName;
|
|
244
|
-
devConfig.main = options.entryPoint;
|
|
245
|
-
devConfig.dev = { ...devConfig.dev, port: devPort };
|
|
246
|
-
delete devConfig.assets;
|
|
247
|
-
// The user can also pass this file to `wrangler types --config
|
|
248
|
-
// .dev-worker-wrangler.jsonc` to get typed bindings like
|
|
249
|
-
// DurableObjectNamespace<EchoDO> resolved from the source entry.
|
|
250
|
-
const devConfigJson = JSON.stringify(devConfig, null, '\t');
|
|
251
|
-
tempConfigPath = resolve('.dev-worker-wrangler.jsonc');
|
|
252
|
-
await writeFile(tempConfigPath, devConfigJson);
|
|
253
|
-
// Write a wrangler config for adapter-cloudflare's getPlatformProxy
|
|
254
|
-
// (used by vite dev for platform.env). It can't run internal DOs or
|
|
255
|
-
// Workflows itself -- those are served by the sidecar above. Rewrite
|
|
256
|
-
// internal DO and Workflow bindings as cross-worker bindings
|
|
257
|
-
// (script_name pointing at the sidecar) so platform.env.MY_DO and
|
|
258
|
-
// platform.env.MY_WORKFLOW calls in +server.ts reach the sidecar via
|
|
259
|
-
// the wrangler dev registry.
|
|
260
|
-
const proxyConfig = structuredClone(baseConfig);
|
|
261
|
-
// Wrangler does not merge per-env `durable_objects`/`workflows`/
|
|
262
|
-
// `migrations` arrays with the top-level — the selected env wholly
|
|
263
|
-
// overrides them. So we patch only the scope wrangler will actually
|
|
264
|
-
// use: top-level when no env, env[envName] when CLOUDFLARE_ENV is set.
|
|
265
|
-
const activeScope = envName ? proxyConfig.env?.[envName] : proxyConfig;
|
|
266
|
-
if (envName && !activeScope) {
|
|
267
|
-
throw new Error(`CLOUDFLARE_ENV="${envName}" but no \`env.${envName}\` block in the wrangler config`);
|
|
268
|
-
}
|
|
269
|
-
if (activeScope.durable_objects?.bindings) {
|
|
270
|
-
activeScope.durable_objects.bindings = activeScope.durable_objects.bindings.map((b) => b.script_name ? b : { ...b, script_name: registeredSidecarName });
|
|
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
|
|
271
393
|
}
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
});
|
|
274
454
|
}
|
|
275
|
-
delete activeScope.migrations;
|
|
276
|
-
proxyConfigPath = resolve('.platform-proxy-wrangler.jsonc');
|
|
277
|
-
await writeFile(proxyConfigPath, JSON.stringify(proxyConfig, null, '\t'));
|
|
278
|
-
const structuredLogsHandler = options.structuredLogsHandler ?? makeDefaultWorkerLogHandler(sidecarName);
|
|
279
|
-
worker = await unstable_startWorker({
|
|
280
|
-
config: tempConfigPath,
|
|
281
|
-
// `testScheduled` mounts a `/__scheduled` endpoint on the sidecar
|
|
282
|
-
// so cron handlers can be invoked manually in dev — wrangler dev
|
|
283
|
-
// never auto-fires crons. Curl
|
|
284
|
-
// `http://localhost:<devPort>/__scheduled?cron=*+*+*+*+*` to
|
|
285
|
-
// trigger your `scheduled` handler.
|
|
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
|
-
},
|
|
299
|
-
build: {
|
|
300
|
-
nodejsCompatMode: (parsedConfig) => getNodejsCompatMode(parsedConfig.compatibility_date, parsedConfig.compatibility_flags)
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
server.httpServer?.on('close', async () => {
|
|
304
|
-
if (worker) {
|
|
305
|
-
await worker.dispose();
|
|
306
|
-
worker = null;
|
|
307
|
-
}
|
|
308
|
-
if (tempConfigPath) {
|
|
309
|
-
await unlink(tempConfigPath).catch(() => { });
|
|
310
|
-
}
|
|
311
|
-
if (proxyConfigPath) {
|
|
312
|
-
await unlink(proxyConfigPath).catch(() => { });
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
455
|
}
|
|
316
|
-
|
|
456
|
+
];
|
|
317
457
|
}
|
|
318
458
|
/**
|
|
319
459
|
* Default handler for sidecar worker logs. Writes each line to stdout (or
|
|
@@ -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.
|
|
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,24 +41,33 @@
|
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
39
43
|
"@release-it/conventional-changelog": "^11.0.1",
|
|
40
|
-
"@sveltejs/kit": "^2.
|
|
41
|
-
"@types/node": "^26.
|
|
42
|
-
"esbuild": "^0.28.
|
|
43
|
-
"miniflare": "^4.
|
|
44
|
-
"release-it": "^20.2.
|
|
45
|
-
"typescript": "^
|
|
46
|
-
"vite": "^8.0
|
|
47
|
-
"vitest": "^4.1.
|
|
48
|
-
"wrangler": "^4.
|
|
44
|
+
"@sveltejs/kit": "^2.70.3",
|
|
45
|
+
"@types/node": "^26.6.2",
|
|
46
|
+
"esbuild": "^0.28.2",
|
|
47
|
+
"miniflare": "^4.20260730.0",
|
|
48
|
+
"release-it": "^20.2.1",
|
|
49
|
+
"typescript": "^7.0.2",
|
|
50
|
+
"vite": "^8.3.0",
|
|
51
|
+
"vitest": "^4.1.11",
|
|
52
|
+
"wrangler": "^4.136.2"
|
|
49
53
|
},
|
|
50
54
|
"scripts": {
|
|
51
55
|
"build": "tsc",
|
|
52
56
|
"test": "vitest run && tsc --noEmit",
|
|
57
|
+
"test:acceptance": "pnpm build && vitest run --config vitest.acceptance.config.ts",
|
|
53
58
|
"prepublishOnly": "tsc",
|
|
54
59
|
"release": "release-it"
|
|
55
60
|
},
|
|
61
|
+
"packageManager": "pnpm@11.9.0",
|
|
62
|
+
"devEngines": {
|
|
63
|
+
"runtime": {
|
|
64
|
+
"name": "node",
|
|
65
|
+
"version": ">=26",
|
|
66
|
+
"onFail": "warn"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
56
69
|
"dependencies": {
|
|
57
70
|
"jsonc-parser": "^3.3.1",
|
|
58
|
-
"smol-toml": "^1.
|
|
71
|
+
"smol-toml": "^1.8.0"
|
|
59
72
|
}
|
|
60
73
|
}
|