@oselvar/sveltekit-add-worker-exports 2.1.1 → 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 CHANGED
@@ -217,6 +217,49 @@ declare global {
217
217
  }
218
218
  ```
219
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
+
220
263
  ## Options
221
264
 
222
265
  | Option | Type | Required | Default | Description |
@@ -232,7 +275,7 @@ declare global {
232
275
 
233
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:
234
277
 
235
- 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-)
236
279
  2. Renames the original `_worker.js` to `_sveltekit_worker.js`
237
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:
238
281
 
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
@@ -82,3 +84,26 @@ export interface AddWorkerExportsOptions {
82
84
  * ```
83
85
  */
84
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
@@ -117,6 +119,27 @@ export function addWorkerExports(options) {
117
119
  * Returns `true` if it patched (or had already patched), `false` if the
118
120
  * adapter hasn't written `_worker.js` yet.
119
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
+ }
120
143
  async function patchWorker(options) {
121
144
  const outputDir = resolve(options.outputDir ?? '.svelte-kit/cloudflare');
122
145
  const workerPath = resolve(outputDir, '_worker.js');
@@ -143,6 +166,10 @@ async function patchWorker(options) {
143
166
  // `require('path')` or `import 'node:async_hooks'` survive bundling;
144
167
  // the Workers runtime resolves them when `nodejs_compat` is enabled
145
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.
146
173
  await build({
147
174
  entryPoints: [options.entryPoint],
148
175
  bundle: true,
@@ -151,6 +178,7 @@ async function patchWorker(options) {
151
178
  target: 'esnext',
152
179
  conditions: ['workerd', 'worker', 'browser'],
153
180
  external: ['cloudflare:*', ...NODE_BUILTINS, ...NODE_BUILTINS.map((m) => `node:${m}`)],
181
+ plugins: [createModuleRulesPlugin(await readModuleRules(options))],
154
182
  outfile: exportsPath
155
183
  });
156
184
  // Rename original worker and create merged entry point.
@@ -210,6 +238,7 @@ function devPlugin(options) {
210
238
  let tempConfigPath = null;
211
239
  let proxyConfigPath = null;
212
240
  async function readRawConfig() {
241
+ // Dynamic import — see readModuleRules for why.
213
242
  const { unstable_readConfig } = await import('wrangler');
214
243
  const parsed = unstable_readConfig(options.wranglerConfig ? { config: options.wranglerConfig } : {});
215
244
  const path = parsed.configPath;
@@ -227,6 +256,7 @@ function devPlugin(options) {
227
256
  };
228
257
  },
229
258
  async configureServer(server) {
259
+ // Dynamic import — see readModuleRules for why.
230
260
  const { unstable_startWorker } = await import('wrangler');
231
261
  const { path: configPath, contents } = await readRawConfig();
232
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.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.66.0",
41
- "@types/node": "^26.0.0",
40
+ "@sveltejs/kit": "^2.69.3",
41
+ "@types/node": "^26.1.1",
42
42
  "esbuild": "^0.28.1",
43
- "miniflare": "^4.20260617.1",
44
- "release-it": "^20.2.0",
45
- "typescript": "^6.0.3",
46
- "vite": "^8.0.16",
47
- "vitest": "^4.1.9",
48
- "wrangler": "^4.103.0"
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.6.1"
67
+ "smol-toml": "^1.7.0"
59
68
  }
60
69
  }