@jundle/cloudflare 0.0.0-stage → 0.0.1-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/package.json +22 -3
- package/src/bindings.js +295 -0
- package/src/build.d.ts +145 -0
- package/src/build.js +327 -0
- package/src/config.js +82 -0
- package/src/harness.js +707 -0
- package/src/index.d.ts +389 -0
- package/src/index.js +446 -0
- package/README.md +0 -3
package/src/index.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// @jundle/cloudflare — the Cloudflare Workers runtime for the
|
|
2
|
+
// jundle dev server (SPEC §16.3). It IS the workerd runtime:
|
|
3
|
+
// a dev entry names it as its `runtime` (SPEC §6.12, §16.2).
|
|
4
|
+
// Miniflare + wrangler are dependencies of THIS package, not
|
|
5
|
+
// jundle core (so apps that only use Node never install
|
|
6
|
+
// workerd). Wrangler is pinned (see config.js) because its
|
|
7
|
+
// config utilities are `unstable_`.
|
|
8
|
+
// Runtime plugin shape (SPEC §16.2): { createRunner,
|
|
9
|
+
// defaultOptions } (a single runtime, not a map). The dev
|
|
10
|
+
// server calls createRunner for a dev entry whose runtime
|
|
11
|
+
// is this package.
|
|
12
|
+
// createRunner({ configDir, entryUrl, exportShape,
|
|
13
|
+
// options }) -> { config, wranglerFile, options,
|
|
14
|
+
// fetch(request), invalidate(invalidated, opts?),
|
|
15
|
+
// dispose(), start({ baseUrl })?, ...extras }
|
|
16
|
+
// options is the entry's options object, opaque to core:
|
|
17
|
+
// wranglerConfig (path, resolved against configDir),
|
|
18
|
+
// remoteBindings (names of remote bindings from the
|
|
19
|
+
// wrangler file), serviceWorkers ({ name: script } local
|
|
20
|
+
// service-binding targets via Miniflare's workers array
|
|
21
|
+
// (CF-3 multi-worker; the main worker stays in the array,
|
|
22
|
+
// since the array replaces the top-level script)).
|
|
23
|
+
// The runner runs a Worker in Miniflare with the full binding
|
|
24
|
+
// surface from the wrangler file. start(script) is the
|
|
25
|
+
// standalone/test path (startScript); the remaining extras
|
|
26
|
+
// (dispatchFetch, dispatchScheduled, getQueueProducer,
|
|
27
|
+
// setScript) are plugin-specific, not part of the interface.
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { Log, LogLevel, Miniflare } from "miniflare";
|
|
31
|
+
import { readWranglerConfig } from "./config.js";
|
|
32
|
+
import { wranglerToMiniflareOptions } from "./bindings.js";
|
|
33
|
+
|
|
34
|
+
// Module-runner harness (SPEC §16.11): the static worker
|
|
35
|
+
// script (harness.js) is read as text and passed to
|
|
36
|
+
// Miniflare as the main worker's script, with generated
|
|
37
|
+
// export wrappers substituted for the placeholder. The
|
|
38
|
+
// harness evaluates app modules inside workerd through the
|
|
39
|
+
// unsafe-eval binding — a dev-only Miniflare option, never
|
|
40
|
+
// a wrangler binding, never production.
|
|
41
|
+
const HARNESS_TEMPLATE = fs.readFileSync(new URL("./harness.js", import.meta.url), "utf8");
|
|
42
|
+
const EVAL_BINDING_NAME = "__jundle_runner_eval";
|
|
43
|
+
const BASE_BINDING_NAME = "__jundle_runner_base";
|
|
44
|
+
const ENTRY_BINDING_NAME = "__jundle_runner_entry";
|
|
45
|
+
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
46
|
+
|
|
47
|
+
// Console forwarding (SPEC §16.11): Miniflare's `log`
|
|
48
|
+
// option is the logger for its own errors, warnings and
|
|
49
|
+
// informative messages. Without it the core service
|
|
50
|
+
// defaults to NoOpLog, so Miniflare's own warnings and
|
|
51
|
+
// errors are swallowed (worker console.log/error reach
|
|
52
|
+
// the terminal anyway, via workerd's stdio pipe —
|
|
53
|
+
// log/info/debug to stdout, warn/error to stderr — and
|
|
54
|
+
// the `log` option does not duplicate them). Miniflare's
|
|
55
|
+
// own Log writes every level to stdout (its log() is
|
|
56
|
+
// console.log for all levels), so this subclass routes
|
|
57
|
+
// by level instead — ERROR/WARN to stderr, the rest to
|
|
58
|
+
// stdout — with the same `[mf:<level>]` prefix shape.
|
|
59
|
+
const LOG_LEVEL_NAME = ["", "error", "warn", "info", "debug", "verbose"];
|
|
60
|
+
class JundleLog extends Log {
|
|
61
|
+
logWithLevel(level, message) {
|
|
62
|
+
if (level <= 0 || level > this.level) return;
|
|
63
|
+
const line = `[mf:${LOG_LEVEL_NAME[level]}] ${message}`;
|
|
64
|
+
if (level <= LogLevel.WARN) console.error(line);
|
|
65
|
+
else console.log(line);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generate the static export wrappers for DO / Workflow /
|
|
70
|
+
// entrypoint class exports (SPEC §16.11). Workers require
|
|
71
|
+
// static exports, so the harness cannot export app classes
|
|
72
|
+
// dynamically: each wrapper extends the real base class,
|
|
73
|
+
// lazily constructs the real class from the evaluated entry
|
|
74
|
+
// (after a version check, so alarms never use a stale
|
|
75
|
+
// class), and delegates every discovered method. The names
|
|
76
|
+
// `__jundleReal` / `__jundleRealInstance` / `__jundleCtx` /
|
|
77
|
+
// `__jundleEnv` are reserved (a real class method of those
|
|
78
|
+
// names is dropped rather than shadowing the
|
|
79
|
+
// lazy-construction machinery).
|
|
80
|
+
function buildWrappers(classes) {
|
|
81
|
+
const bases = {
|
|
82
|
+
"durable-object": "cwNs.DurableObject",
|
|
83
|
+
workflow: "cwNs.WorkflowEntrypoint",
|
|
84
|
+
entrypoint: "cwNs.WorkerEntrypoint",
|
|
85
|
+
};
|
|
86
|
+
return (classes || [])
|
|
87
|
+
.map(({ name, kind, methods }) => {
|
|
88
|
+
if (!IDENT.test(name))
|
|
89
|
+
throw new Error(
|
|
90
|
+
`CF plugin: refusing to generate wrapper for non-identifier export '${name}'`,
|
|
91
|
+
);
|
|
92
|
+
const base = bases[kind];
|
|
93
|
+
if (!base) throw new Error(`CF plugin: unknown export class kind '${kind}' for '${name}'`);
|
|
94
|
+
const methodLines = (methods || [])
|
|
95
|
+
.filter(
|
|
96
|
+
(m) =>
|
|
97
|
+
IDENT.test(m) &&
|
|
98
|
+
m !== "constructor" &&
|
|
99
|
+
m !== "__jundleReal" &&
|
|
100
|
+
m !== "__jundleRealInstance" &&
|
|
101
|
+
m !== "__jundleCtx" &&
|
|
102
|
+
m !== "__jundleEnv",
|
|
103
|
+
)
|
|
104
|
+
.map(
|
|
105
|
+
(m) =>
|
|
106
|
+
` async ${m}(...args) { return (await this.__jundleReal())[${JSON.stringify(m)}](...args); }`,
|
|
107
|
+
);
|
|
108
|
+
return `export class ${name} extends ${base} {
|
|
109
|
+
constructor(ctx, env) {
|
|
110
|
+
super(ctx, env);
|
|
111
|
+
this.__jundleCtx = ctx;
|
|
112
|
+
this.__jundleEnv = env;
|
|
113
|
+
this.__jundleRealInstance = null;
|
|
114
|
+
}
|
|
115
|
+
async __jundleReal() {
|
|
116
|
+
if (!this.__jundleRealInstance) {
|
|
117
|
+
const m = await __jundleImportEntry();
|
|
118
|
+
const C = m[${JSON.stringify(name)}];
|
|
119
|
+
if (typeof C !== "function")
|
|
120
|
+
throw new Error("ModuleRunner: entry export " + ${JSON.stringify(name)} + " is not a class in the evaluated entry.");
|
|
121
|
+
this.__jundleRealInstance = new C(this.__jundleCtx, stripEnv(this.__jundleEnv));
|
|
122
|
+
}
|
|
123
|
+
return this.__jundleRealInstance;
|
|
124
|
+
}
|
|
125
|
+
${methodLines.join("\n")}
|
|
126
|
+
}`;
|
|
127
|
+
})
|
|
128
|
+
.join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildHarnessScript(exportShape) {
|
|
132
|
+
// Anchor on the standalone placeholder line: the header
|
|
133
|
+
// comment mentions the marker name, so a bare replace
|
|
134
|
+
// would substitute there instead of at the real line.
|
|
135
|
+
const replaced = HARNESS_TEMPLATE.replace(
|
|
136
|
+
"\n// __JUNDLE_WRAPPERS__\n",
|
|
137
|
+
`\n${buildWrappers(exportShape && exportShape.classes)}\n`,
|
|
138
|
+
);
|
|
139
|
+
if (replaced === HARNESS_TEMPLATE)
|
|
140
|
+
throw new Error("CF plugin: harness template is missing its __JUNDLE_WRAPPERS__ line");
|
|
141
|
+
return replaced;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Find the built server entry chunk in a bundle dir.
|
|
145
|
+
// Primary signal: server-manifest.json's `entries` —
|
|
146
|
+
// the entry carrying a `runtime` is the process entry
|
|
147
|
+
// (SPEC §6.12). Chunk file names are content-hashed
|
|
148
|
+
// and change with the chunking policy, so no name
|
|
149
|
+
// pattern or content sniffing is the primary lookup.
|
|
150
|
+
// Fallback, for bundles built before `entries` existed:
|
|
151
|
+
// it is the entry chunk that is NOT any page's
|
|
152
|
+
// entryFile in page-manifest.json (pages are
|
|
153
|
+
// server-entry modules; the runner is the remaining
|
|
154
|
+
// server entry).
|
|
155
|
+
// Workers require a static module graph: a variable dynamic
|
|
156
|
+
// import (import(chunkId)) cannot be resolved, even if it is
|
|
157
|
+
// never called. The browser shim (src/runtime/shim.js) has
|
|
158
|
+
// exactly that — its loadChunk dynamic-imports the chunk URL
|
|
159
|
+
// string (SPEC §6.11 manifest-free browser loading). SSR in
|
|
160
|
+
// the Worker never calls it (SSR loads chunks through the
|
|
161
|
+
// server shim's literal-import manifest), so the Worker's
|
|
162
|
+
// copy of the bundle patches it to a throw. The browser still
|
|
163
|
+
// gets the ORIGINAL bundle (served by the dev server as a dev
|
|
164
|
+
// module, or in prod from the client dir at /assets/*), so
|
|
165
|
+
// hydration is unaffected. Only files
|
|
166
|
+
// containing a variable import are patched; a file with no
|
|
167
|
+
// such import is copied verbatim.
|
|
168
|
+
export async function createRunner({
|
|
169
|
+
configDir,
|
|
170
|
+
entryUrl,
|
|
171
|
+
exportShape,
|
|
172
|
+
options: entryOptions = {},
|
|
173
|
+
}) {
|
|
174
|
+
const wranglerConfig = entryOptions.wranglerConfig
|
|
175
|
+
? path.resolve(configDir, entryOptions.wranglerConfig)
|
|
176
|
+
: undefined;
|
|
177
|
+
const serviceWorkers = entryOptions.serviceWorkers;
|
|
178
|
+
const {
|
|
179
|
+
file: wranglerFile,
|
|
180
|
+
config,
|
|
181
|
+
devVars,
|
|
182
|
+
} = await readWranglerConfig({ configDir, wranglerConfig });
|
|
183
|
+
const options = wranglerToMiniflareOptions({
|
|
184
|
+
config,
|
|
185
|
+
devVars,
|
|
186
|
+
configDir,
|
|
187
|
+
wranglerFile,
|
|
188
|
+
remoteBindings: entryOptions.remoteBindings,
|
|
189
|
+
});
|
|
190
|
+
let mf = null;
|
|
191
|
+
// Runner state (SPEC §16.11): the entry dev URL + export
|
|
192
|
+
// shape from creation, and the dev-server base URL once
|
|
193
|
+
// start() runs after the server binds.
|
|
194
|
+
let runnerState = { entryUrl, exportShape, baseUrl: null };
|
|
195
|
+
|
|
196
|
+
// Build the Miniflare constructor args for a main worker.
|
|
197
|
+
// Single-worker (no serviceWorkers): the current top-level
|
|
198
|
+
// form (script or scriptPath). Multi-worker: Miniflare's
|
|
199
|
+
// `workers` array REPLACES the top-level script (CF-1
|
|
200
|
+
// quirk), so the main worker must be IN the array, carrying
|
|
201
|
+
// its bindings, with the Shared persist/compat options at
|
|
202
|
+
// top level. Unknown keys are stripped by Miniflare's Zod
|
|
203
|
+
// schemas, so passing the full mapped options on the main
|
|
204
|
+
// worker entry is safe (Shared-only keys like persist paths
|
|
205
|
+
// are stripped there and land via the top-level shared).
|
|
206
|
+
// Logging (see JundleLog above): pass a Log so
|
|
207
|
+
// Miniflare's own warnings/errors reach the terminal, and
|
|
208
|
+
// disable per-request logging — the dev server prints its
|
|
209
|
+
// own boot lines and no request logs, so Miniflare's
|
|
210
|
+
// "[mf:info] GET / 200 OK" lines would be noise. The
|
|
211
|
+
// `log` option is a shared (core) option, so it lands at
|
|
212
|
+
// top level in both forms.
|
|
213
|
+
const logging = { log: new JundleLog(LogLevel.INFO), logRequests: false };
|
|
214
|
+
function buildArgs(mainScript) {
|
|
215
|
+
if (!serviceWorkers || !Object.keys(serviceWorkers).length) {
|
|
216
|
+
return { ...options, ...logging, ...mainScript };
|
|
217
|
+
}
|
|
218
|
+
const shared = { ...logging };
|
|
219
|
+
for (const k of [
|
|
220
|
+
"kvPersist",
|
|
221
|
+
"r2Persist",
|
|
222
|
+
"d1Persist",
|
|
223
|
+
"durableObjectsPersist",
|
|
224
|
+
"workflowsPersist",
|
|
225
|
+
"compatibilityDate",
|
|
226
|
+
"compatibilityFlags",
|
|
227
|
+
]) {
|
|
228
|
+
if (options[k] !== undefined) shared[k] = options[k];
|
|
229
|
+
}
|
|
230
|
+
const mainWorker = { name: options.name, ...options, ...mainScript };
|
|
231
|
+
const workers = [mainWorker];
|
|
232
|
+
for (const [name, script] of Object.entries(serviceWorkers)) {
|
|
233
|
+
// Service scripts are ESM (export default); without
|
|
234
|
+
// modules:true Miniflare parses them as CommonJS and
|
|
235
|
+
// `export` is a syntax error (same .js->CJS default as
|
|
236
|
+
// the CF-2 bundle chunks).
|
|
237
|
+
workers.push({ name, script, modules: true });
|
|
238
|
+
}
|
|
239
|
+
// The unsafe-eval binding is a per-worker option in the
|
|
240
|
+
// array form (and top-level in the single-worker form);
|
|
241
|
+
// include it in the shared options too, so it lands
|
|
242
|
+
// whichever schema Miniflare validates against. Unknown
|
|
243
|
+
// keys are stripped, so the duplicate is harmless.
|
|
244
|
+
if (mainScript.unsafeEvalBinding) shared.unsafeEvalBinding = mainScript.unsafeEvalBinding;
|
|
245
|
+
return { ...shared, workers };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function applyMigrations(target) {
|
|
249
|
+
// D1 migrations (mapped in bindings.js as d1Migrations,
|
|
250
|
+
// NOT a Miniflare option — wrangler dev applies them
|
|
251
|
+
// itself). After Miniflare starts, execute each migration
|
|
252
|
+
// file's SQL against its D1 binding via getD1Database.
|
|
253
|
+
// `target` lets a reload run migrations against the new
|
|
254
|
+
// instance before it is swapped in; defaults to the live one.
|
|
255
|
+
// Applied files are tracked per binding in a
|
|
256
|
+
// __jundle_migrations table (same idea as wrangler's own
|
|
257
|
+
// tracking): a reconstruction or bundle reload builds a
|
|
258
|
+
// NEW Miniflare against the SAME persisted D1, and
|
|
259
|
+
// re-running CREATE TABLE fails with "table already
|
|
260
|
+
// exists" (and re-running data migrations would
|
|
261
|
+
// duplicate rows). New files still apply; a file whose
|
|
262
|
+
// exec fails is not recorded, so it retries next time.
|
|
263
|
+
const inst = target || mf;
|
|
264
|
+
for (const m of options.d1Migrations || []) {
|
|
265
|
+
const db = await inst.getD1Database(m.binding);
|
|
266
|
+
await db.exec("CREATE TABLE IF NOT EXISTS __jundle_migrations (filename TEXT PRIMARY KEY)");
|
|
267
|
+
const applied = new Set(
|
|
268
|
+
(await db.prepare("SELECT filename FROM __jundle_migrations").all()).results.map(
|
|
269
|
+
(r) => r.filename,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
for (const f of fs
|
|
273
|
+
.readdirSync(m.dir)
|
|
274
|
+
.filter((x) => x.endsWith(".sql"))
|
|
275
|
+
.sort()) {
|
|
276
|
+
if (applied.has(f)) continue;
|
|
277
|
+
await db.exec(fs.readFileSync(path.join(m.dir, f), "utf8"));
|
|
278
|
+
await db.prepare("INSERT INTO __jundle_migrations (filename) VALUES (?)").bind(f).run();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function startRunnerInternal({ entryUrl, baseUrl, exportShape }) {
|
|
284
|
+
// The harness statically imports node: builtins, so it
|
|
285
|
+
// requires nodejs_compat from the wrangler file (the
|
|
286
|
+
// single source of truth — jundle never injects
|
|
287
|
+
// compatibility flags). Without it, Miniflare fails
|
|
288
|
+
// the first dispatch with a cryptic string-script
|
|
289
|
+
// module error; fail fast with the actual fix instead.
|
|
290
|
+
if (!(options.compatibilityFlags || []).includes("nodejs_compat")) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`CF plugin: the module runner requires "nodejs_compat" in compatibility_flags in ${wranglerFile} (the harness imports node: builtins); add it to the wrangler file.`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
// One static harness worker per session: the harness
|
|
296
|
+
// script (with wrappers for the export shape) + the
|
|
297
|
+
// unsafe-eval binding + the wrangler-derived bindings
|
|
298
|
+
// (merged with the harness's internal bindings, which
|
|
299
|
+
// the harness strips before app code sees env). App code
|
|
300
|
+
// is never a Miniflare option. Construct-before-dispose
|
|
301
|
+
// applies here too (see invalidate).
|
|
302
|
+
const script = buildHarnessScript(exportShape);
|
|
303
|
+
const harnessBindings = {
|
|
304
|
+
...options.bindings,
|
|
305
|
+
[BASE_BINDING_NAME]: baseUrl,
|
|
306
|
+
[ENTRY_BINDING_NAME]: entryUrl,
|
|
307
|
+
};
|
|
308
|
+
const builtArgs = buildArgs({
|
|
309
|
+
script,
|
|
310
|
+
modules: true,
|
|
311
|
+
unsafeEvalBinding: EVAL_BINDING_NAME,
|
|
312
|
+
bindings: harnessBindings,
|
|
313
|
+
});
|
|
314
|
+
const next = new Miniflare(builtArgs);
|
|
315
|
+
try {
|
|
316
|
+
await applyMigrations(next);
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// Mirror the bundle path: a failed construction must
|
|
319
|
+
// not leak its workerd child (an undisposed instance
|
|
320
|
+
// keeps the dev-server process alive at exit).
|
|
321
|
+
await next.dispose().catch(() => {});
|
|
322
|
+
throw e;
|
|
323
|
+
}
|
|
324
|
+
return next;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
config,
|
|
329
|
+
wranglerFile,
|
|
330
|
+
options,
|
|
331
|
+
// Uniform runner interface (SPEC §16.2): fetch serves
|
|
332
|
+
// the app, invalidate reconstructs on an export-shape
|
|
333
|
+
// change (otherwise a no-op — the harness pulls the
|
|
334
|
+
// recorded batch via the version route on next
|
|
335
|
+
// dispatch), dispose tears down Miniflare, and start
|
|
336
|
+
// (optional) starts the harness once the dev server
|
|
337
|
+
// has bound (it needs the base URL).
|
|
338
|
+
async fetch(request) {
|
|
339
|
+
if (!mf) throw new Error("CF plugin: runner not started (call start first)");
|
|
340
|
+
// Miniflare's dispatchFetch does a cross-realm
|
|
341
|
+
// instanceof Request check that Node's Request fails,
|
|
342
|
+
// falling through to new URL("[object Request]") —
|
|
343
|
+
// so dispatch uses plain values (URL string, plain
|
|
344
|
+
// headers object, string body), NOT the incoming
|
|
345
|
+
// Request object. Both parts must be forwarded:
|
|
346
|
+
// dropping init silently downgrades every request to
|
|
347
|
+
// a bare GET, so POST /action arrived as GET /action
|
|
348
|
+
// and the app 404'd on route no-match (GETs worked,
|
|
349
|
+
// which hid the bug).
|
|
350
|
+
const init = {
|
|
351
|
+
method: request.method,
|
|
352
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
353
|
+
};
|
|
354
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
355
|
+
init.body = await request.text();
|
|
356
|
+
}
|
|
357
|
+
return mf.dispatchFetch(request.url, init);
|
|
358
|
+
},
|
|
359
|
+
async invalidate(_invalidated, opts) {
|
|
360
|
+
if (!opts?.exportShape) return;
|
|
361
|
+
if (!mf || !runnerState) throw new Error("CF plugin: cannot reconstruct before start");
|
|
362
|
+
// Export-shape change (SPEC §16.11): the export set is
|
|
363
|
+
// construction-time, so a new DO / Workflow / entrypoint
|
|
364
|
+
// class export (or a removed one) requires rebuilding
|
|
365
|
+
// Miniflare with new wrappers. Same bindings and
|
|
366
|
+
// persistence root (wrangler file unchanged), and the
|
|
367
|
+
// new instance is built before the old one is disposed.
|
|
368
|
+
const next = await startRunnerInternal({
|
|
369
|
+
entryUrl: runnerState.entryUrl,
|
|
370
|
+
baseUrl: runnerState.baseUrl,
|
|
371
|
+
exportShape: opts.exportShape,
|
|
372
|
+
});
|
|
373
|
+
const oldMf = mf;
|
|
374
|
+
mf = next;
|
|
375
|
+
runnerState = { ...runnerState, exportShape: opts.exportShape };
|
|
376
|
+
await oldMf.dispose();
|
|
377
|
+
// Observable like the "built in" line: a
|
|
378
|
+
// reconstruction costs a fresh Miniflare, so the dev
|
|
379
|
+
// server says so instead of silently stalling; tests
|
|
380
|
+
// capture this line as the positive reconstruction
|
|
381
|
+
// signal.
|
|
382
|
+
console.log("jundle: runner reconstructed (export shape changed)");
|
|
383
|
+
},
|
|
384
|
+
async start({ baseUrl }) {
|
|
385
|
+
if (mf) await mf.dispose();
|
|
386
|
+
mf = await startRunnerInternal({
|
|
387
|
+
entryUrl: runnerState.entryUrl,
|
|
388
|
+
baseUrl,
|
|
389
|
+
exportShape: runnerState.exportShape,
|
|
390
|
+
});
|
|
391
|
+
runnerState = { ...runnerState, baseUrl };
|
|
392
|
+
},
|
|
393
|
+
// Standalone/test path: start Miniflare with a given
|
|
394
|
+
// script (no harness). Not part of the interface.
|
|
395
|
+
async startScript(script) {
|
|
396
|
+
if (mf) await mf.dispose();
|
|
397
|
+
mf = new Miniflare(buildArgs({ script }));
|
|
398
|
+
await applyMigrations();
|
|
399
|
+
return mf;
|
|
400
|
+
},
|
|
401
|
+
// dispatchFetch takes (url, init) — plain values, not a
|
|
402
|
+
// Request object (Miniflare's dispatchFetch does a
|
|
403
|
+
// cross-realm instanceof check that Node's Request
|
|
404
|
+
// fails). Both args must be forwarded: dropping init
|
|
405
|
+
// silently downgrades every request to a bare GET, so POST
|
|
406
|
+
// /action arrived as GET /action and the app 404'd on route
|
|
407
|
+
// no-match (GETs worked, which hid the bug).
|
|
408
|
+
async dispatchFetch(url, init) {
|
|
409
|
+
if (!mf) throw new Error("CF plugin: runner not started (call start first)");
|
|
410
|
+
return mf.dispatchFetch(url, init);
|
|
411
|
+
},
|
|
412
|
+
async setScript(script) {
|
|
413
|
+
if (!mf) throw new Error("CF plugin: runner not started (call start first)");
|
|
414
|
+
await mf.setOptions({ script });
|
|
415
|
+
},
|
|
416
|
+
// Event handlers (SPEC §16.3, CF-3): fetch via
|
|
417
|
+
// dispatchFetch (above), plus scheduled + queue.
|
|
418
|
+
// Scheduled: Miniflare's getWorker().scheduled({ cron })
|
|
419
|
+
// (the documented dispatch path; there is no
|
|
420
|
+
// dispatchScheduled on Miniflare itself in v4).
|
|
421
|
+
async dispatchScheduled(cron) {
|
|
422
|
+
if (!mf) throw new Error("CF plugin: runner not started (call start first)");
|
|
423
|
+
const worker = await mf.getWorker();
|
|
424
|
+
return worker.scheduled({ cron: cron || "" });
|
|
425
|
+
},
|
|
426
|
+
// Queue: producers send (Miniflare delivers to the
|
|
427
|
+
// consumer Worker's queue() handler — the real path, not
|
|
428
|
+
// a synthetic dispatch). This returns the producer for a
|
|
429
|
+
// binding so callers can send; the consumer runs
|
|
430
|
+
// automatically on delivery.
|
|
431
|
+
async getQueueProducer(binding) {
|
|
432
|
+
if (!mf) throw new Error("CF plugin: runner not started (call start first)");
|
|
433
|
+
return mf.getQueueProducer(binding);
|
|
434
|
+
},
|
|
435
|
+
async dispose() {
|
|
436
|
+
if (mf) {
|
|
437
|
+
await mf.dispose();
|
|
438
|
+
mf = null;
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export const defaultOptions = { wranglerConfig: null };
|
|
445
|
+
|
|
446
|
+
export default { createRunner, defaultOptions };
|
package/README.md
DELETED