wrangler 2.0.23 → 2.0.26

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.
Files changed (80) hide show
  1. package/README.md +20 -2
  2. package/bin/wrangler.js +1 -1
  3. package/miniflare-dist/index.mjs +235 -47
  4. package/package.json +11 -6
  5. package/src/__tests__/configuration.test.ts +89 -17
  6. package/src/__tests__/dev.test.tsx +29 -4
  7. package/src/__tests__/generate.test.ts +93 -0
  8. package/src/__tests__/helpers/mock-cfetch.ts +87 -2
  9. package/src/__tests__/index.test.ts +10 -27
  10. package/src/__tests__/init.test.ts +537 -359
  11. package/src/__tests__/jest.setup.ts +34 -1
  12. package/src/__tests__/kv.test.ts +2 -2
  13. package/src/__tests__/metrics.test.ts +5 -0
  14. package/src/__tests__/pages.test.ts +14 -0
  15. package/src/__tests__/publish.test.ts +497 -254
  16. package/src/__tests__/r2.test.ts +173 -71
  17. package/src/__tests__/tail.test.ts +112 -42
  18. package/src/__tests__/user.test.ts +1 -0
  19. package/src/__tests__/validate-dev-props.test.ts +56 -0
  20. package/src/__tests__/whoami.test.tsx +60 -1
  21. package/src/api/dev.ts +7 -0
  22. package/src/bundle.ts +279 -44
  23. package/src/cfetch/internal.ts +73 -2
  24. package/src/config/config.ts +8 -3
  25. package/src/config/environment.ts +40 -8
  26. package/src/config/index.ts +13 -0
  27. package/src/config/validation.ts +102 -8
  28. package/src/create-worker-upload-form.ts +25 -0
  29. package/src/dev/dev.tsx +121 -28
  30. package/src/dev/local.tsx +88 -14
  31. package/src/dev/remote.tsx +39 -8
  32. package/src/dev/use-esbuild.ts +28 -0
  33. package/src/dev/validate-dev-props.ts +31 -0
  34. package/src/dev-registry.tsx +160 -0
  35. package/src/dev.tsx +107 -80
  36. package/src/generate.ts +112 -14
  37. package/src/index.tsx +212 -4
  38. package/src/init.ts +111 -38
  39. package/src/inspect.ts +90 -5
  40. package/src/metrics/index.ts +1 -0
  41. package/src/metrics/metrics-dispatcher.ts +1 -0
  42. package/src/metrics/metrics-usage-headers.ts +24 -0
  43. package/src/metrics/send-event.ts +2 -2
  44. package/src/miniflare-cli/assets.ts +27 -16
  45. package/src/miniflare-cli/index.ts +124 -2
  46. package/src/module-collection.ts +3 -3
  47. package/src/pages/build.tsx +75 -41
  48. package/src/pages/constants.ts +5 -0
  49. package/src/pages/deployments.tsx +10 -10
  50. package/src/pages/dev.tsx +177 -52
  51. package/src/pages/errors.ts +22 -0
  52. package/src/pages/functions/buildPlugin.ts +4 -0
  53. package/src/pages/functions/buildWorker.ts +4 -0
  54. package/src/pages/functions/routes-consolidation.test.ts +250 -0
  55. package/src/pages/functions/routes-consolidation.ts +73 -0
  56. package/src/pages/functions/routes-transformation.test.ts +271 -0
  57. package/src/pages/functions/routes-transformation.ts +122 -0
  58. package/src/pages/functions.tsx +96 -0
  59. package/src/pages/index.tsx +65 -55
  60. package/src/pages/projects.tsx +9 -3
  61. package/src/pages/publish.tsx +76 -23
  62. package/src/pages/types.ts +9 -0
  63. package/src/pages/upload.tsx +38 -21
  64. package/src/publish.ts +126 -112
  65. package/src/r2.ts +81 -0
  66. package/src/tail/filters.ts +3 -1
  67. package/src/tail/index.ts +15 -2
  68. package/src/tail/printing.ts +43 -3
  69. package/src/user/user.tsx +20 -2
  70. package/src/whoami.tsx +79 -1
  71. package/src/worker.ts +12 -0
  72. package/templates/first-party-worker-module-facade.ts +18 -0
  73. package/templates/format-dev-errors.ts +32 -0
  74. package/templates/pages-template-plugin.ts +16 -4
  75. package/templates/pages-template-worker.ts +16 -5
  76. package/templates/{static-asset-facade.js → serve-static-assets.ts} +21 -7
  77. package/templates/service-bindings-module-facade.js +54 -0
  78. package/templates/service-bindings-sw-facade.js +42 -0
  79. package/wrangler-dist/cli.d.ts +7 -0
  80. package/wrangler-dist/cli.js +40851 -15332
package/src/init.ts CHANGED
@@ -5,12 +5,16 @@ import TOML from "@iarna/toml";
5
5
  import { findUp } from "find-up";
6
6
  import { version as wranglerVersion } from "../package.json";
7
7
 
8
+ import { fetchDashboardScript } from "./cfetch/internal";
9
+ import { readConfig } from "./config";
8
10
  import { confirm, select } from "./dialogs";
9
11
  import { initializeGit, isGitInstalled, isInsideGitRepo } from "./git-client";
10
12
  import { logger } from "./logger";
11
13
  import { getPackageManager } from "./package-manager";
12
14
  import { parsePackageJSON, parseTOML, readFileSync } from "./parse";
15
+ import { requireAuth } from "./user";
13
16
  import { CommandLineArgsError, printWranglerBanner } from "./index";
17
+ import type { ConfigPath } from "./index";
14
18
 
15
19
  import type { Argv, ArgumentsCamelCase } from "yargs";
16
20
 
@@ -36,6 +40,12 @@ export async function initOptions(yargs: Argv) {
36
40
  describe: 'Answer "yes" to any prompts for new projects',
37
41
  type: "boolean",
38
42
  alias: "y",
43
+ })
44
+ .option("from-dash", {
45
+ describe: "Download script from the dashboard for local development",
46
+ type: "string",
47
+ requiresArg: true,
48
+ hidden: true,
39
49
  });
40
50
  }
41
51
 
@@ -61,7 +71,11 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
61
71
  const devDepsToInstall: string[] = [];
62
72
  const instructions: string[] = [];
63
73
  let shouldRunPackageManagerInstall = false;
64
- const creationDirectory = path.resolve(process.cwd(), args.name ?? "");
74
+ const fromDashScriptName = args["from-dash"] as string;
75
+ const creationDirectory = path.resolve(
76
+ process.cwd(),
77
+ (args.name ? args.name : fromDashScriptName) ?? ""
78
+ );
65
79
 
66
80
  if (args.site) {
67
81
  const gitDirectory =
@@ -90,6 +104,7 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
90
104
 
91
105
  // TODO: ask which directory to make the worker in (defaults to args.name)
92
106
  // TODO: if args.name isn't provided, ask what to name the worker
107
+ // Note: `--from-dash` will be a fallback creationDir/Worker name if none is provided.
93
108
 
94
109
  const wranglerTomlDestination = path.join(
95
110
  creationDirectory,
@@ -98,12 +113,15 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
98
113
  let justCreatedWranglerToml = false;
99
114
 
100
115
  if (fs.existsSync(wranglerTomlDestination)) {
116
+ let shouldContinue = false;
101
117
  logger.warn(
102
118
  `${path.relative(process.cwd(), wranglerTomlDestination)} already exists!`
103
119
  );
104
- const shouldContinue = await confirm(
105
- "Do you want to continue initializing this project?"
106
- );
120
+ if (!fromDashScriptName) {
121
+ shouldContinue = await confirm(
122
+ "Do you want to continue initializing this project?"
123
+ );
124
+ }
107
125
  if (!shouldContinue) {
108
126
  return;
109
127
  }
@@ -438,27 +456,23 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
438
456
  process.cwd(),
439
457
  path.join(creationDirectory, "./src/index.ts")
440
458
  );
441
-
442
- const newWorkerType = yesFlag
443
- ? "fetch"
444
- : await getNewWorkerType(newWorkerFilename);
445
-
446
- if (newWorkerType !== "none") {
447
- const template = getNewWorkerTemplate("ts", newWorkerType);
448
-
459
+ if (fromDashScriptName) {
460
+ const config = readConfig(args.config as ConfigPath, args);
461
+ const accountId = await requireAuth(config);
449
462
  await mkdir(path.join(creationDirectory, "./src"), {
450
463
  recursive: true,
451
464
  });
452
- await writeFile(
453
- path.join(creationDirectory, "./src/index.ts"),
454
- readFileSync(path.join(__dirname, `../templates/${template}`))
465
+
466
+ const dashScript = await fetchDashboardScript(
467
+ `/accounts/${accountId}/workers/scripts/${fromDashScriptName}`,
468
+ {
469
+ method: "GET",
470
+ }
455
471
  );
456
472
 
457
- logger.log(
458
- `✨ Created ${path.relative(
459
- process.cwd(),
460
- path.join(creationDirectory, "./src/index.ts")
461
- )}`
473
+ await writeFile(
474
+ path.join(creationDirectory, "./src/index.ts"),
475
+ dashScript
462
476
  );
463
477
 
464
478
  await writePackageJsonScriptsAndUpdateWranglerToml(
@@ -466,8 +480,39 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
466
480
  justCreatedWranglerToml,
467
481
  pathToPackageJson,
468
482
  "src/index.ts",
469
- getNewWorkerToml(newWorkerType)
483
+ {}
470
484
  );
485
+ } else {
486
+ const newWorkerType = yesFlag
487
+ ? "fetch"
488
+ : await getNewWorkerType(newWorkerFilename);
489
+
490
+ if (newWorkerType !== "none") {
491
+ const template = getNewWorkerTemplate("ts", newWorkerType);
492
+
493
+ await mkdir(path.join(creationDirectory, "./src"), {
494
+ recursive: true,
495
+ });
496
+ await writeFile(
497
+ path.join(creationDirectory, "./src/index.ts"),
498
+ readFileSync(path.join(__dirname, `../templates/${template}`))
499
+ );
500
+
501
+ logger.log(
502
+ `✨ Created ${path.relative(
503
+ process.cwd(),
504
+ path.join(creationDirectory, "./src/index.ts")
505
+ )}`
506
+ );
507
+
508
+ await writePackageJsonScriptsAndUpdateWranglerToml(
509
+ shouldWritePackageJsonScripts,
510
+ justCreatedWranglerToml,
511
+ pathToPackageJson,
512
+ "src/index.ts",
513
+ getNewWorkerToml(newWorkerType)
514
+ );
515
+ }
471
516
  }
472
517
  }
473
518
  } else {
@@ -477,35 +522,63 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
477
522
  path.join(creationDirectory, "./src/index.js")
478
523
  );
479
524
 
480
- const newWorkerType = yesFlag
481
- ? "fetch"
482
- : await getNewWorkerType(newWorkerFilename);
483
-
484
- if (newWorkerType !== "none") {
485
- const template = getNewWorkerTemplate("js", newWorkerType);
486
-
525
+ if (fromDashScriptName) {
526
+ const config = readConfig(args.config as ConfigPath, args);
527
+ const accountId = await requireAuth(config);
487
528
  await mkdir(path.join(creationDirectory, "./src"), {
488
529
  recursive: true,
489
530
  });
490
- await writeFile(
491
- path.join(creationDirectory, "./src/index.js"),
492
- readFileSync(path.join(__dirname, `../templates/${template}`))
531
+
532
+ const dashScript = await fetchDashboardScript(
533
+ `/accounts/${accountId}/workers/scripts/${fromDashScriptName}`,
534
+ {
535
+ method: "GET",
536
+ }
493
537
  );
494
538
 
495
- logger.log(
496
- `✨ Created ${path.relative(
497
- process.cwd(),
498
- path.join(creationDirectory, "./src/index.js")
499
- )}`
539
+ await writeFile(
540
+ path.join(creationDirectory, "./src/index.js"),
541
+ dashScript
500
542
  );
501
543
 
502
544
  await writePackageJsonScriptsAndUpdateWranglerToml(
503
545
  shouldWritePackageJsonScripts,
504
546
  justCreatedWranglerToml,
505
547
  pathToPackageJson,
506
- "src/index.js",
507
- getNewWorkerToml(newWorkerType)
548
+ "src/index.ts",
549
+ {}
508
550
  );
551
+ } else {
552
+ const newWorkerType = yesFlag
553
+ ? "fetch"
554
+ : await getNewWorkerType(newWorkerFilename);
555
+
556
+ if (newWorkerType !== "none") {
557
+ const template = getNewWorkerTemplate("js", newWorkerType);
558
+
559
+ await mkdir(path.join(creationDirectory, "./src"), {
560
+ recursive: true,
561
+ });
562
+ await writeFile(
563
+ path.join(creationDirectory, "./src/index.js"),
564
+ readFileSync(path.join(__dirname, `../templates/${template}`))
565
+ );
566
+
567
+ logger.log(
568
+ `✨ Created ${path.relative(
569
+ process.cwd(),
570
+ path.join(creationDirectory, "./src/index.js")
571
+ )}`
572
+ );
573
+
574
+ await writePackageJsonScriptsAndUpdateWranglerToml(
575
+ shouldWritePackageJsonScripts,
576
+ justCreatedWranglerToml,
577
+ pathToPackageJson,
578
+ "src/index.js",
579
+ getNewWorkerToml(newWorkerType)
580
+ );
581
+ }
509
582
  }
510
583
  }
511
584
  }
package/src/inspect.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFile } from "fs/promises";
1
2
  import assert from "node:assert";
2
3
  import { createServer } from "node:http";
3
4
  import os from "node:os";
@@ -5,6 +6,7 @@ import { URL } from "node:url";
5
6
 
6
7
  import open from "open";
7
8
  import { useEffect, useRef, useState } from "react";
9
+ import { SourceMapConsumer } from "source-map";
8
10
  import WebSocket, { WebSocketServer } from "ws";
9
11
  import { version } from "../package.json";
10
12
  import { logger } from "./logger";
@@ -52,6 +54,10 @@ interface InspectorProps {
52
54
  * logged to the terminal by nature of them actually running in node locally.)
53
55
  */
54
56
  logToTerminal: boolean;
57
+ /**
58
+ * Sourcemap path, so that stacktraces can be interpretted
59
+ */
60
+ sourceMapPath?: string | undefined;
55
61
  }
56
62
 
57
63
  export default function useInspector(props: InspectorProps) {
@@ -252,15 +258,93 @@ export default function useInspector(props: InspectorProps) {
252
258
  * without having to open the devtools).
253
259
  */
254
260
  if (props.logToTerminal) {
255
- ws.addEventListener("message", (event: MessageEvent) => {
261
+ ws.addEventListener("message", async (event: MessageEvent) => {
256
262
  if (typeof event.data === "string") {
257
263
  const evt = JSON.parse(event.data);
258
264
  if (evt.method === "Runtime.exceptionThrown") {
259
265
  const params = evt.params as Protocol.Runtime.ExceptionThrownEvent;
260
- logger.error(
261
- params.exceptionDetails.text,
262
- params.exceptionDetails.exception?.description ?? ""
263
- );
266
+
267
+ // Parse stack trace with source map.
268
+ if (props.sourceMapPath) {
269
+ // Parse in the sourcemap
270
+ const mapContent = JSON.parse(
271
+ await readFile(props.sourceMapPath, "utf-8")
272
+ );
273
+
274
+ // Create the lines for the exception details log
275
+ const exceptionLines = [
276
+ params.exceptionDetails.exception?.description?.split("\n")[0],
277
+ ];
278
+
279
+ await SourceMapConsumer.with(
280
+ mapContent,
281
+ null,
282
+ async (consumer) => {
283
+ // Pass each of the callframes into the consumer, and format the error
284
+ const stack = params.exceptionDetails.stackTrace?.callFrames;
285
+
286
+ stack?.forEach(
287
+ ({ functionName, lineNumber, columnNumber }, i) => {
288
+ try {
289
+ if (lineNumber) {
290
+ // The line and column numbers in the stackTrace are zero indexed,
291
+ // whereas the sourcemap consumer indexes from one.
292
+ const pos = consumer.originalPositionFor({
293
+ line: lineNumber + 1,
294
+ column: columnNumber + 1,
295
+ });
296
+
297
+ // Print out line which caused error:
298
+ if (i === 0 && pos.source && pos.line) {
299
+ const fileSource = consumer.sourceContentFor(
300
+ pos.source
301
+ );
302
+ const fileSourceLine =
303
+ fileSource?.split("\n")[pos.line - 1] || "";
304
+ exceptionLines.push(fileSourceLine.trim());
305
+
306
+ // If we have a column, we can mark the position underneath
307
+ if (pos.column) {
308
+ exceptionLines.push(
309
+ `${" ".repeat(
310
+ pos.column - fileSourceLine.search(/\S/)
311
+ )}^`
312
+ );
313
+ }
314
+ }
315
+
316
+ // From the way esbuild implements the "names" field:
317
+ // > To save space, the original name is only recorded when it's different from the final name.
318
+ // however, source-map consumer does not handle this
319
+ if (pos && pos.line != null) {
320
+ const convertedFnName =
321
+ pos.name || functionName || "";
322
+ exceptionLines.push(
323
+ ` at ${convertedFnName} (${pos.source}:${pos.line}:${pos.column})`
324
+ );
325
+ }
326
+ }
327
+ } catch {
328
+ // Line failed to parse through the sourcemap consumer
329
+ // We should handle this better
330
+ }
331
+ }
332
+ );
333
+ }
334
+ );
335
+
336
+ // Log the parsed stacktrace
337
+ logger.error(
338
+ params.exceptionDetails.text,
339
+ exceptionLines.join("\n")
340
+ );
341
+ } else {
342
+ // We log the stacktrace to the terminal
343
+ logger.error(
344
+ params.exceptionDetails.text,
345
+ params.exceptionDetails.exception?.description ?? ""
346
+ );
347
+ }
264
348
  }
265
349
  if (evt.method === "Runtime.consoleAPICalled") {
266
350
  logConsoleMessage(
@@ -333,6 +417,7 @@ export default function useInspector(props: InspectorProps) {
333
417
  }, [
334
418
  props.inspectorUrl,
335
419
  props.logToTerminal,
420
+ props.sourceMapPath,
336
421
  wsServer,
337
422
  // We use a state value as a sigil to trigger a retry of the
338
423
  // remote websocket connection. It's not used inside the effect,
@@ -2,3 +2,4 @@ export { getMetricsDispatcher } from "./metrics-dispatcher";
2
2
  export type { Properties } from "./metrics-dispatcher";
3
3
  export { getMetricsConfig } from "./metrics-config";
4
4
  export * from "./send-event";
5
+ export { getMetricsUsageHeaders } from "./metrics-usage-headers";
@@ -67,6 +67,7 @@ export async function getMetricsDispatcher(options: MetricsConfigOptions) {
67
67
  properties: {
68
68
  category: "Workers",
69
69
  wranglerVersion,
70
+ os: process.platform + ":" + process.arch,
70
71
  ...event.properties,
71
72
  },
72
73
  });
@@ -0,0 +1,24 @@
1
+ import { getMetricsConfig } from "./metrics-config";
2
+
3
+ /**
4
+ * Add an additional header to publish requests if the user has opted into sending usage metrics.
5
+ *
6
+ * This allows us to estimate the number of instances of Wrangler that have opted-in
7
+ * without breaking our agreement not to send stuff if you have not opted-in.
8
+ */
9
+ export async function getMetricsUsageHeaders(
10
+ sendMetrics: boolean | undefined
11
+ ): Promise<Record<string, string> | undefined> {
12
+ const metricsEnabled = (
13
+ await getMetricsConfig({
14
+ sendMetrics,
15
+ })
16
+ ).enabled;
17
+ if (metricsEnabled) {
18
+ return {
19
+ metricsEnabled: "true",
20
+ };
21
+ } else {
22
+ return undefined;
23
+ }
24
+ }
@@ -47,8 +47,8 @@ export type EventNames =
47
47
  | "rename worker namespace"
48
48
  | "create pages project"
49
49
  | "list pages projects"
50
- | "deploy pages project"
51
- | "list pages projects deployments"
50
+ | "create pages deployment"
51
+ | "list pages deployments"
52
52
  | "build pages functions"
53
53
  | "run dev"
54
54
  | "run pages dev";
@@ -335,6 +335,11 @@ async function generateAssetsFetch(
335
335
 
336
336
  const generateResponse = (request: MiniflareRequest) => {
337
337
  const url = new URL(request.url);
338
+ let assetName = url.pathname;
339
+ try {
340
+ //it's possible for someone to send a URL like http://fakehost/abc%2 which would fail to decode
341
+ assetName = decodeURIComponent(url.pathname);
342
+ } catch {}
338
343
 
339
344
  const deconstructedResponse: {
340
345
  status: number;
@@ -377,7 +382,7 @@ async function generateAssetsFetch(
377
382
  }
378
383
 
379
384
  const notFound = () => {
380
- let cwd = url.pathname;
385
+ let cwd = assetName;
381
386
  while (cwd) {
382
387
  cwd = cwd.slice(0, cwd.lastIndexOf("/"));
383
388
 
@@ -407,38 +412,36 @@ async function generateAssetsFetch(
407
412
 
408
413
  let asset;
409
414
 
410
- if (url.pathname.endsWith("/")) {
411
- if ((asset = getAsset(`${url.pathname}/index.html`))) {
415
+ if (assetName.endsWith("/")) {
416
+ if ((asset = getAsset(`${assetName}/index.html`))) {
412
417
  deconstructedResponse.body = serveAsset(asset);
413
418
  deconstructedResponse.headers.set(
414
419
  "Content-Type",
415
420
  getType(asset) || "application/octet-stream"
416
421
  );
417
422
  return deconstructedResponse;
418
- } else if (
419
- (asset = getAsset(`${url.pathname.replace(/\/$/, ".html")}`))
420
- ) {
423
+ } else if ((asset = getAsset(`${assetName.replace(/\/$/, ".html")}`))) {
421
424
  deconstructedResponse.status = 301;
422
425
  deconstructedResponse.headers.set(
423
426
  "Location",
424
- `${url.pathname.slice(0, -1)}${url.search}`
427
+ `${assetName.slice(0, -1)}${url.search}`
425
428
  );
426
429
  return deconstructedResponse;
427
430
  }
428
431
  }
429
432
 
430
- if (url.pathname.endsWith("/index")) {
433
+ if (assetName.endsWith("/index")) {
431
434
  deconstructedResponse.status = 301;
432
435
  deconstructedResponse.headers.set(
433
436
  "Location",
434
- `${url.pathname.slice(0, -"index".length)}${url.search}`
437
+ `${assetName.slice(0, -"index".length)}${url.search}`
435
438
  );
436
439
  return deconstructedResponse;
437
440
  }
438
441
 
439
- if ((asset = getAsset(url.pathname))) {
440
- if (url.pathname.endsWith(".html")) {
441
- const extensionlessPath = url.pathname.slice(0, -".html".length);
442
+ if ((asset = getAsset(assetName))) {
443
+ if (assetName.endsWith(".html")) {
444
+ const extensionlessPath = assetName.slice(0, -".html".length);
442
445
  if (getAsset(extensionlessPath) || extensionlessPath === "/") {
443
446
  deconstructedResponse.body = serveAsset(asset);
444
447
  deconstructedResponse.headers.set(
@@ -462,12 +465,20 @@ async function generateAssetsFetch(
462
465
  );
463
466
  return deconstructedResponse;
464
467
  }
465
- } else if (hasFileExtension(url.pathname)) {
468
+ } else if (hasFileExtension(assetName)) {
469
+ if ((asset = getAsset(assetName + ".html"))) {
470
+ deconstructedResponse.body = serveAsset(asset);
471
+ deconstructedResponse.headers.set(
472
+ "Content-Type",
473
+ getType(asset) || "application/octet-stream"
474
+ );
475
+ return deconstructedResponse;
476
+ }
466
477
  notFound();
467
478
  return deconstructedResponse;
468
479
  }
469
480
 
470
- if ((asset = getAsset(`${url.pathname}.html`))) {
481
+ if ((asset = getAsset(`${assetName}.html`))) {
471
482
  deconstructedResponse.body = serveAsset(asset);
472
483
  deconstructedResponse.headers.set(
473
484
  "Content-Type",
@@ -476,11 +487,11 @@ async function generateAssetsFetch(
476
487
  return deconstructedResponse;
477
488
  }
478
489
 
479
- if ((asset = getAsset(`${url.pathname}/index.html`))) {
490
+ if ((asset = getAsset(`${assetName}/index.html`))) {
480
491
  deconstructedResponse.status = 301;
481
492
  deconstructedResponse.headers.set(
482
493
  "Location",
483
- `${url.pathname}/${url.search}`
494
+ `${assetName}/${url.search}`
484
495
  );
485
496
  return deconstructedResponse;
486
497
  } else {
@@ -1,10 +1,23 @@
1
- import { Log, LogLevel, Miniflare } from "miniflare";
1
+ import { fetch } from "@miniflare/core";
2
+ import {
3
+ DurableObjectNamespace,
4
+ DurableObjectStub,
5
+ } from "@miniflare/durable-objects";
6
+ import {
7
+ Log,
8
+ LogLevel,
9
+ Miniflare,
10
+ Response as MiniflareResponse,
11
+ Request as MiniflareRequest,
12
+ } from "miniflare";
2
13
  import yargs from "yargs";
3
14
  import { hideBin } from "yargs/helpers";
15
+ import { FatalError } from "../errors";
4
16
  import generateASSETSBinding from "./assets";
5
17
  import { enumKeys } from "./enum-keys";
6
18
  import { getRequestContextCheckOptions } from "./request-context";
7
19
  import type { Options } from "./assets";
20
+ import type { AddressInfo } from "net";
8
21
 
9
22
  export interface EnablePagesAssetsServiceBindingOptions {
10
23
  proxyPort?: number;
@@ -44,7 +57,48 @@ async function main() {
44
57
  console.log("OPTIONS:\n", JSON.stringify(config, null, 2));
45
58
  }
46
59
 
60
+ config.bindings = {
61
+ ...config.bindings,
62
+ ...Object.fromEntries(
63
+ Object.entries(
64
+ config.externalDurableObjects as Record<
65
+ string,
66
+ { name: string; host: string; port: number }
67
+ >
68
+ ).map(([binding, { name, host, port }]) => {
69
+ const factory = () => {
70
+ throw new FatalError(
71
+ "An external Durable Object instance's state has somehow been attempted to be accessed.",
72
+ 1
73
+ );
74
+ };
75
+ const namespace = new DurableObjectNamespace(name as string, factory);
76
+ namespace.get = (id) => {
77
+ const stub = new DurableObjectStub(factory, id);
78
+ stub.fetch = (...reqArgs) => {
79
+ const requestFromArgs = new MiniflareRequest(...reqArgs);
80
+ const url = new URL(requestFromArgs.url);
81
+ url.host = host;
82
+ if (port !== undefined) url.port = port.toString();
83
+ const request = new MiniflareRequest(
84
+ url.toString(),
85
+ requestFromArgs
86
+ );
87
+ request.headers.set("x-miniflare-durable-object-name", name);
88
+ request.headers.set("x-miniflare-durable-object-id", id.toString());
89
+
90
+ return fetch(request);
91
+ };
92
+ return stub;
93
+ };
94
+ return [binding, namespace];
95
+ })
96
+ ),
97
+ };
98
+
47
99
  let mf: Miniflare | undefined;
100
+ let durableObjectsMf: Miniflare | undefined = undefined;
101
+ let durableObjectsMfPort: number | undefined = undefined;
48
102
 
49
103
  try {
50
104
  if (args._[1]) {
@@ -73,12 +127,80 @@ async function main() {
73
127
  // Start Miniflare development server
74
128
  await mf.startServer();
75
129
  await mf.startScheduler();
76
- process.send && process.send("ready");
130
+
131
+ const internalDurableObjectClassNames = Object.values(
132
+ config.durableObjects as Record<string, string>
133
+ );
134
+
135
+ if (internalDurableObjectClassNames.length > 0) {
136
+ durableObjectsMf = new Miniflare({
137
+ host: config.host,
138
+ port: 0,
139
+ script: `
140
+ export default {
141
+ fetch(request, env) {
142
+ return env.DO.fetch(request)
143
+ }
144
+ }`,
145
+ serviceBindings: {
146
+ DO: async (request: MiniflareRequest) => {
147
+ request = new MiniflareRequest(request);
148
+
149
+ const name = request.headers.get("x-miniflare-durable-object-name");
150
+ const idString = request.headers.get(
151
+ "x-miniflare-durable-object-id"
152
+ );
153
+ request.headers.delete("x-miniflare-durable-object-name");
154
+ request.headers.delete("x-miniflare-durable-object-id");
155
+
156
+ if (!name || !idString) {
157
+ return new MiniflareResponse(
158
+ "[durable-object-proxy-err] Missing `x-miniflare-durable-object-name` or `x-miniflare-durable-object-id` headers.",
159
+ { status: 400 }
160
+ );
161
+ }
162
+
163
+ const namespace = await mf?.getDurableObjectNamespace(name);
164
+ const id = namespace?.idFromString(idString);
165
+
166
+ if (!id) {
167
+ return new MiniflareResponse(
168
+ "[durable-object-proxy-err] Could not generate an ID. Possibly due to a mismatched DO name and ID?",
169
+ { status: 500 }
170
+ );
171
+ }
172
+
173
+ const stub = namespace?.get(id);
174
+
175
+ if (!stub) {
176
+ return new MiniflareResponse(
177
+ "[durable-object-proxy-err] Could not generate a stub. Possibly due to a mismatched DO name and ID?",
178
+ { status: 500 }
179
+ );
180
+ }
181
+
182
+ return stub.fetch(request);
183
+ },
184
+ },
185
+ modules: true,
186
+ });
187
+ const server = await durableObjectsMf.startServer();
188
+ durableObjectsMfPort = (server.address() as AddressInfo).port;
189
+ }
190
+
191
+ process.send &&
192
+ process.send(
193
+ JSON.stringify({
194
+ ready: true,
195
+ durableObjectsPort: durableObjectsMfPort,
196
+ })
197
+ );
77
198
  } catch (e) {
78
199
  mf?.log.error(e as Error);
79
200
  process.exitCode = 1;
80
201
  // Unmount any mounted workers
81
202
  await mf?.dispose();
203
+ await durableObjectsMf?.dispose();
82
204
  }
83
205
  }
84
206
 
@@ -123,9 +123,9 @@ export default function createModuleCollector(props: {
123
123
  {
124
124
  filter: new RegExp(
125
125
  "^(" +
126
- [...props.wrangler1xlegacyModuleReferences.fileNames].join(
127
- "|"
128
- ) +
126
+ [...props.wrangler1xlegacyModuleReferences.fileNames]
127
+ .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
128
+ .join("|") +
129
129
  ")$"
130
130
  ),
131
131
  },