wrangler 0.0.0-e928f94 → 0.0.0-e95bea2

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 (100) hide show
  1. package/bin/wrangler.js +2 -2
  2. package/package.json +46 -18
  3. package/pages/functions/buildWorker.ts +62 -0
  4. package/pages/functions/filepath-routing.test.ts +123 -0
  5. package/pages/functions/filepath-routing.ts +178 -0
  6. package/pages/functions/identifiers.ts +78 -0
  7. package/pages/functions/routes.ts +151 -0
  8. package/pages/functions/template-worker.ts +130 -0
  9. package/src/__tests__/configuration.test.ts +2217 -0
  10. package/src/__tests__/dev.test.tsx +405 -0
  11. package/src/__tests__/dev2.test.tsx +152 -0
  12. package/src/__tests__/guess-worker-format.test.ts +58 -0
  13. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  14. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  15. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  16. package/src/__tests__/helpers/mock-account-id.ts +44 -0
  17. package/src/__tests__/helpers/mock-bin.ts +36 -0
  18. package/src/__tests__/helpers/mock-cfetch.ts +207 -0
  19. package/src/__tests__/helpers/mock-console.ts +70 -0
  20. package/src/__tests__/helpers/mock-dialogs.ts +65 -0
  21. package/src/__tests__/helpers/mock-kv.ts +40 -0
  22. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  23. package/src/__tests__/helpers/mock-user.ts +27 -0
  24. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  25. package/src/__tests__/helpers/run-in-tmp.ts +29 -0
  26. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  27. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  28. package/src/__tests__/https-options.test.ts +127 -0
  29. package/src/__tests__/index.test.ts +957 -119
  30. package/src/__tests__/jest.setup.ts +69 -0
  31. package/src/__tests__/kv.test.ts +1408 -0
  32. package/src/__tests__/logout.test.ts +50 -0
  33. package/src/__tests__/package-manager.test.ts +206 -0
  34. package/src/__tests__/pages.test.ts +44 -0
  35. package/src/__tests__/parse.test.ts +322 -0
  36. package/src/__tests__/publish.test.ts +2453 -0
  37. package/src/__tests__/r2.test.ts +198 -0
  38. package/src/__tests__/route.test.ts +45 -0
  39. package/src/__tests__/secret.test.ts +425 -0
  40. package/src/__tests__/sentry.test.ts +146 -0
  41. package/src/__tests__/tail.test.ts +564 -0
  42. package/src/__tests__/whoami.test.tsx +86 -0
  43. package/src/bundle.ts +142 -0
  44. package/src/cfetch/index.ts +104 -0
  45. package/src/cfetch/internal.ts +135 -0
  46. package/src/cli.ts +16 -7
  47. package/src/config/README.md +107 -0
  48. package/src/config/config.ts +279 -0
  49. package/src/config/diagnostics.ts +80 -0
  50. package/src/config/environment.ts +305 -0
  51. package/src/config/index.ts +52 -0
  52. package/src/config/validation-helpers.ts +388 -0
  53. package/src/config/validation.ts +1027 -0
  54. package/src/{api/preview.ts → create-worker-preview.ts} +63 -25
  55. package/src/create-worker-upload-form.ts +217 -0
  56. package/src/dev/dev.tsx +357 -0
  57. package/src/dev/local.tsx +241 -0
  58. package/src/dev/remote.tsx +201 -0
  59. package/src/dev/use-esbuild.ts +100 -0
  60. package/src/dialogs.tsx +5 -13
  61. package/src/entry.ts +159 -0
  62. package/src/environment-variables.ts +33 -0
  63. package/src/https-options.ts +122 -0
  64. package/src/index.tsx +1374 -903
  65. package/src/inspect.ts +617 -0
  66. package/src/kv/index.tsx +339 -0
  67. package/src/module-collection.ts +215 -26
  68. package/src/open-in-browser.ts +25 -0
  69. package/src/package-manager.ts +126 -0
  70. package/src/pages.tsx +452 -250
  71. package/src/parse.ts +183 -0
  72. package/src/paths.ts +26 -0
  73. package/src/proxy.ts +438 -0
  74. package/src/publish.ts +300 -292
  75. package/src/r2.ts +50 -0
  76. package/src/reporting.ts +131 -0
  77. package/src/selfsigned.d.ts +29 -0
  78. package/src/sites.tsx +181 -58
  79. package/src/tail/filters.ts +279 -0
  80. package/src/tail/index.ts +299 -0
  81. package/src/tail/printing.ts +62 -0
  82. package/src/user.tsx +166 -130
  83. package/src/whoami.tsx +69 -0
  84. package/src/worker.ts +163 -0
  85. package/templates/new-worker.js +15 -0
  86. package/templates/new-worker.ts +15 -0
  87. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  88. package/templates/tsconfig.json +105 -0
  89. package/wrangler-dist/cli.js +131958 -115793
  90. package/wrangler-dist/cli.js.map +3 -3
  91. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  92. package/src/__tests__/mock-cfetch.js +0 -46
  93. package/src/api/form_data.ts +0 -141
  94. package/src/api/inspect.ts +0 -441
  95. package/src/api/worker.ts +0 -161
  96. package/src/cfetch.ts +0 -72
  97. package/src/config.ts +0 -122
  98. package/src/dev.tsx +0 -724
  99. package/src/kv.tsx +0 -211
  100. package/src/tail.tsx +0 -71
package/src/pages.tsx CHANGED
@@ -1,24 +1,49 @@
1
- import type { BuilderCallback } from "yargs";
2
- import { join } from "path";
3
- import { tmpdir } from "os";
4
- import { existsSync, lstatSync, readFileSync } from "fs";
5
- import { execSync, spawn } from "child_process";
6
- import { Headers, Request, Response } from "undici";
7
- import type { MiniflareOptions } from "miniflare";
8
- import type { RequestInfo, RequestInit } from "@miniflare/core";
9
- import open from "open";
1
+ /* eslint-disable no-shadow */
2
+
3
+ import { execSync, spawn } from "node:child_process";
4
+ import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { URL } from "node:url";
10
8
  import { watch } from "chokidar";
9
+ import { getType } from "mime";
10
+ import { buildWorker } from "../pages/functions/buildWorker";
11
+ import { generateConfigFromFileTree } from "../pages/functions/filepath-routing";
12
+ import { writeRoutesModule } from "../pages/functions/routes";
13
+ import openInBrowser from "./open-in-browser";
14
+ import { toUrlPath } from "./paths";
15
+ import type { Config } from "../pages/functions/routes";
16
+ import type { Headers, Request, fetch } from "@miniflare/core";
17
+ import type { BuildResult } from "esbuild";
18
+ import type { MiniflareOptions } from "miniflare";
19
+ import type { BuilderCallback } from "yargs";
20
+
21
+ // Defer importing miniflare until we really need it. This takes ~0.5s
22
+ // and also modifies some `stream/web` and `undici` prototypes, so we
23
+ // don't want to do this if pages commands aren't being called.
24
+
25
+ const EXIT_CALLBACKS: (() => void)[] = [];
26
+ const EXIT = (message?: string, code?: number) => {
27
+ if (message) console.log(message);
28
+ if (code) process.exitCode = code;
29
+ EXIT_CALLBACKS.forEach((callback) => callback());
30
+ process.exit(code);
31
+ };
11
32
 
12
- type Exit = (message?: string) => undefined;
33
+ process.on("SIGINT", () => EXIT());
34
+ process.on("SIGTERM", () => EXIT());
13
35
 
14
- const isWindows = () => process.platform === "win32";
36
+ function isWindows() {
37
+ return process.platform === "win32";
38
+ }
15
39
 
16
40
  const SECONDS_TO_WAIT_FOR_PROXY = 5;
17
41
 
18
- const sleep = async (ms: number) =>
42
+ async function sleep(ms: number) {
19
43
  await new Promise((resolve) => setTimeout(resolve, ms));
44
+ }
20
45
 
21
- const getPids = (pid: number) => {
46
+ function getPids(pid: number) {
22
47
  const pids: number[] = [pid];
23
48
  let command: string, regExp: RegExp;
24
49
 
@@ -43,9 +68,9 @@ const getPids = (pid: number) => {
43
68
  } catch {}
44
69
 
45
70
  return pids;
46
- };
71
+ }
47
72
 
48
- const getPort = (pid: number) => {
73
+ function getPort(pid: number) {
49
74
  let command: string, regExp: RegExp;
50
75
 
51
76
  if (isWindows()) {
@@ -70,24 +95,19 @@ const getPort = (pid: number) => {
70
95
  `Error scanning for ports of process with PID ${pid}: ${thrown}`
71
96
  );
72
97
  }
73
- };
98
+ }
74
99
 
75
- const spawnProxyProcess = async ({
100
+ async function spawnProxyProcess({
76
101
  port,
77
102
  command,
78
103
  }: {
79
104
  port?: number;
80
105
  command: (string | number)[];
81
- }) => {
82
- const exit: Exit = (message) => {
83
- if (message) console.error(message);
84
- if (proxy) proxy.kill();
85
- return undefined;
86
- };
87
-
106
+ }): Promise<void | number> {
88
107
  if (command.length === 0)
89
- return exit(
90
- "Must specify a directory of static assets to serve or a command to run."
108
+ return EXIT(
109
+ "Must specify a directory of static assets to serve or a command to run.",
110
+ 1
91
111
  );
92
112
 
93
113
  console.log(`Running ${command.join(" ")}...`);
@@ -102,6 +122,9 @@ const spawnProxyProcess = async ({
102
122
  },
103
123
  }
104
124
  );
125
+ EXIT_CALLBACKS.push(() => {
126
+ proxy.kill();
127
+ });
105
128
 
106
129
  proxy.stdout.on("data", (data) => {
107
130
  console.log(`[proxy]: ${data}`);
@@ -130,34 +153,35 @@ const spawnProxyProcess = async ({
130
153
  .filter((port) => port !== undefined)[0];
131
154
 
132
155
  if (port === undefined) {
133
- return exit(
134
- "Could not automatically determine proxy port. Please specify the proxy port with --proxy."
156
+ return EXIT(
157
+ "Could not automatically determine proxy port. Please specify the proxy port with --proxy.",
158
+ 1
135
159
  );
136
160
  } else {
137
161
  console.log(`Automatically determined the proxy port to be ${port}.`);
138
162
  }
139
163
  }
140
164
 
141
- return { port, exit };
142
- };
165
+ return port;
166
+ }
143
167
 
144
- const escapeRegex = (str: string) => {
168
+ function escapeRegex(str: string) {
145
169
  return str.replace(/[-/\\^$*+?.()|[]{}]/g, "\\$&");
146
- };
170
+ }
147
171
 
148
- export type Replacements = Record<string, string>;
172
+ type Replacements = Record<string, string>;
149
173
 
150
- export const replacer = (str: string, replacements: Replacements) => {
174
+ function replacer(str: string, replacements: Replacements) {
151
175
  for (const [replacement, value] of Object.entries(replacements)) {
152
176
  str = str.replace(`:${replacement}`, value);
153
177
  }
154
178
  return str;
155
- };
179
+ }
156
180
 
157
- export const generateRulesMatcher = <T,>(
181
+ function generateRulesMatcher<T>(
158
182
  rules?: Record<string, T>,
159
183
  replacer: (match: T, replacements: Replacements) => T = (match) => match
160
- ) => {
184
+ ) {
161
185
  // TODO: How can you test cross-host rules?
162
186
  if (!rules) return () => [];
163
187
 
@@ -204,9 +228,9 @@ export const generateRulesMatcher = <T,>(
204
228
  })
205
229
  .filter((value) => value !== undefined) as T[];
206
230
  };
207
- };
231
+ }
208
232
 
209
- const generateHeadersMatcher = (headersFile: string) => {
233
+ function generateHeadersMatcher(headersFile: string) {
210
234
  if (existsSync(headersFile)) {
211
235
  const contents = readFileSync(headersFile).toString();
212
236
 
@@ -273,9 +297,9 @@ const generateHeadersMatcher = (headersFile: string) => {
273
297
  } else {
274
298
  return () => undefined;
275
299
  }
276
- };
300
+ }
277
301
 
278
- const generateRedirectsMatcher = (redirectsFile: string) => {
302
+ function generateRedirectsMatcher(redirectsFile: string) {
279
303
  if (existsSync(redirectsFile)) {
280
304
  const contents = readFileSync(redirectsFile).toString();
281
305
 
@@ -322,26 +346,26 @@ const generateRedirectsMatcher = (redirectsFile: string) => {
322
346
  } else {
323
347
  return () => undefined;
324
348
  }
325
- };
349
+ }
326
350
 
327
- const extractPathname = (
351
+ function extractPathname(
328
352
  path = "/",
329
353
  includeSearch: boolean,
330
354
  includeHash: boolean
331
- ) => {
355
+ ) {
332
356
  if (!path.startsWith("/")) path = `/${path}`;
333
357
  const url = new URL(`//${path}`, "relative://");
334
358
  return `${url.pathname}${includeSearch ? url.search : ""}${
335
359
  includeHash ? url.hash : ""
336
360
  }`;
337
- };
361
+ }
338
362
 
339
- const validateURL = (
363
+ function validateURL(
340
364
  token: string,
341
365
  onlyRelative = false,
342
366
  includeSearch = false,
343
367
  includeHash = false
344
- ) => {
368
+ ) {
345
369
  const host = /^https:\/\/+(?<host>[^/]+)\/?(?<path>.*)/.exec(token);
346
370
  if (host && host.groups && host.groups.host) {
347
371
  if (onlyRelative) return;
@@ -362,14 +386,16 @@ const validateURL = (
362
386
  }
363
387
  }
364
388
  return "";
365
- };
389
+ }
366
390
 
367
- const hasFileExtension = (pathname: string) =>
368
- /\/.+\.[a-z0-9]+$/i.test(pathname);
391
+ function hasFileExtension(pathname: string) {
392
+ return /\/.+\.[a-z0-9]+$/i.test(pathname);
393
+ }
394
+
395
+ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
396
+ // Defer importing miniflare until we really need it
397
+ const { Headers, Request, Response } = await import("@miniflare/core");
369
398
 
370
- const generateAssetsFetch = async (
371
- directory: string
372
- ): Promise<typeof fetch> => {
373
399
  const headersFile = join(directory, "_headers");
374
400
  const redirectsFile = join(directory, "_redirects");
375
401
  const workerFile = join(directory, "_worker.js");
@@ -466,14 +492,25 @@ const generateAssetsFetch = async (
466
492
  if ((asset = getAsset(`${cwd}/404.html`))) {
467
493
  deconstructedResponse.status = 404;
468
494
  deconstructedResponse.body = serveAsset(asset);
495
+ deconstructedResponse.headers.set(
496
+ "Content-Type",
497
+ getType(asset) || "application/octet-stream"
498
+ );
469
499
  return deconstructedResponse;
470
500
  }
471
501
  }
472
502
 
473
503
  if ((asset = getAsset(`/index.html`))) {
474
504
  deconstructedResponse.body = serveAsset(asset);
505
+ deconstructedResponse.headers.set(
506
+ "Content-Type",
507
+ getType(asset) || "application/octet-stream"
508
+ );
475
509
  return deconstructedResponse;
476
510
  }
511
+
512
+ deconstructedResponse.status = 404;
513
+ return deconstructedResponse;
477
514
  };
478
515
 
479
516
  let asset;
@@ -481,6 +518,10 @@ const generateAssetsFetch = async (
481
518
  if (url.pathname.endsWith("/")) {
482
519
  if ((asset = getAsset(`${url.pathname}/index.html`))) {
483
520
  deconstructedResponse.body = serveAsset(asset);
521
+ deconstructedResponse.headers.set(
522
+ "Content-Type",
523
+ getType(asset) || "application/octet-stream"
524
+ );
484
525
  return deconstructedResponse;
485
526
  } else if (
486
527
  (asset = getAsset(`${url.pathname.replace(/\/$/, ".html")}`))
@@ -508,6 +549,10 @@ const generateAssetsFetch = async (
508
549
  const extensionlessPath = url.pathname.slice(0, -".html".length);
509
550
  if (getAsset(extensionlessPath) || extensionlessPath === "/") {
510
551
  deconstructedResponse.body = serveAsset(asset);
552
+ deconstructedResponse.headers.set(
553
+ "Content-Type",
554
+ getType(asset) || "application/octet-stream"
555
+ );
511
556
  return deconstructedResponse;
512
557
  } else {
513
558
  deconstructedResponse.status = 301;
@@ -519,6 +564,10 @@ const generateAssetsFetch = async (
519
564
  }
520
565
  } else {
521
566
  deconstructedResponse.body = serveAsset(asset);
567
+ deconstructedResponse.headers.set(
568
+ "Content-Type",
569
+ getType(asset) || "application/octet-stream"
570
+ );
522
571
  return deconstructedResponse;
523
572
  }
524
573
  } else if (hasFileExtension(url.pathname)) {
@@ -528,6 +577,10 @@ const generateAssetsFetch = async (
528
577
 
529
578
  if ((asset = getAsset(`${url.pathname}.html`))) {
530
579
  deconstructedResponse.body = serveAsset(asset);
580
+ deconstructedResponse.headers.set(
581
+ "Content-Type",
582
+ getType(asset) || "application/octet-stream"
583
+ );
531
584
  return deconstructedResponse;
532
585
  }
533
586
 
@@ -569,7 +622,7 @@ const generateAssetsFetch = async (
569
622
  });
570
623
  };
571
624
 
572
- return (async (input, init) => {
625
+ return async (input, init) => {
573
626
  const request = new Request(input, init);
574
627
  const deconstructedResponse = generateResponse(request);
575
628
  attachHeaders(request, deconstructedResponse);
@@ -584,210 +637,276 @@ const generateAssetsFetch = async (
584
637
  headers,
585
638
  status: deconstructedResponse.status,
586
639
  });
587
- }) as any;
588
- };
640
+ };
641
+ }
642
+
643
+ const RUNNING_BUILDERS: BuildResult[] = [];
644
+
645
+ async function buildFunctions({
646
+ scriptPath,
647
+ outputConfigPath,
648
+ functionsDirectory,
649
+ minify = false,
650
+ sourcemap = false,
651
+ fallbackService = "ASSETS",
652
+ watch = false,
653
+ onEnd,
654
+ }: {
655
+ scriptPath: string;
656
+ outputConfigPath?: string;
657
+ functionsDirectory: string;
658
+ minify?: boolean;
659
+ sourcemap?: boolean;
660
+ fallbackService?: string;
661
+ watch?: boolean;
662
+ onEnd?: () => void;
663
+ }) {
664
+ RUNNING_BUILDERS.forEach(
665
+ (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
666
+ );
589
667
 
590
- export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
591
- return yargs.command(
592
- "dev [directory] [-- command]",
593
- "🧑‍💻 Develop your full-stack Pages application locally",
594
- (yargs) => {
595
- return yargs
596
- .positional("directory", {
597
- type: "string",
598
- demandOption: undefined,
599
- description: "The directory of static assets to serve",
600
- })
601
- .positional("command", {
602
- type: "string",
603
- demandOption: undefined,
604
- description: "The proxy command to run",
605
- })
606
- .options({
607
- local: {
608
- type: "boolean",
609
- default: true,
610
- description: "Run on my machine",
611
- },
612
- port: {
613
- type: "number",
614
- default: 8788,
615
- description: "The port to listen on (serve from)",
616
- },
617
- proxy: {
618
- type: "number",
619
- description:
620
- "The port to proxy (where the static assets are served)",
621
- },
622
- "script-path": {
623
- type: "string",
624
- default: "_worker.js",
625
- description:
626
- "The location of the single Worker script if not using functions",
627
- },
628
- binding: {
629
- type: "array",
630
- description: "Bind variable/secret (KEY=VALUE)",
631
- alias: "b",
632
- },
633
- kv: {
634
- type: "array",
635
- description: "KV namespace to bind",
636
- alias: "k",
637
- },
638
- do: {
639
- type: "array",
640
- description: "Durable Object to bind (NAME=CLASS)",
641
- alias: "o",
642
- },
643
- // TODO: Miniflare user options
644
- });
645
- },
646
- async ({
647
- local,
648
- directory,
649
- port,
650
- proxy: requestedProxyPort,
651
- "script-path": singleWorkerScriptPath,
652
- binding: bindings = [],
653
- kv: kvs = [],
654
- do: durableObjects = [],
655
- "--": remaining = [],
656
- }) => {
657
- if (!local) {
658
- console.error("Only local mode is supported at the moment.");
659
- return;
660
- }
668
+ const routesModule = join(tmpdir(), "./functionsRoutes.mjs");
669
+ const baseURL = toUrlPath("/");
661
670
 
662
- const functionsDirectory = "./functions";
663
- const usingFunctions = existsSync(functionsDirectory);
671
+ const config: Config = await generateConfigFromFileTree({
672
+ baseDir: functionsDirectory,
673
+ baseURL,
674
+ });
664
675
 
665
- const command = remaining as (string | number)[];
676
+ if (outputConfigPath) {
677
+ writeFileSync(
678
+ outputConfigPath,
679
+ JSON.stringify({ ...config, baseURL }, null, 2)
680
+ );
681
+ }
666
682
 
667
- let proxyPort: number | undefined;
668
- let exit: Exit = (message) => {
669
- console.error(message);
670
- return undefined;
671
- };
683
+ await writeRoutesModule({
684
+ config,
685
+ srcDir: functionsDirectory,
686
+ outfile: routesModule,
687
+ });
672
688
 
673
- if (directory === undefined) {
674
- const proxy = await spawnProxyProcess({
675
- port: requestedProxyPort,
676
- command,
677
- });
678
- if (proxy === undefined) return undefined;
689
+ RUNNING_BUILDERS.push(
690
+ await buildWorker({
691
+ routesModule,
692
+ outfile: scriptPath,
693
+ minify,
694
+ sourcemap,
695
+ fallbackService,
696
+ watch,
697
+ onEnd,
698
+ })
699
+ );
700
+ }
679
701
 
680
- exit = proxy.exit;
681
- proxyPort = proxy.port;
702
+ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
703
+ return yargs
704
+ .command(
705
+ "dev [directory] [-- command]",
706
+ "🧑‍💻 Develop your full-stack Pages application locally",
707
+ (yargs) => {
708
+ return yargs
709
+ .positional("directory", {
710
+ type: "string",
711
+ demandOption: undefined,
712
+ description: "The directory of static assets to serve",
713
+ })
714
+ .positional("command", {
715
+ type: "string",
716
+ demandOption: undefined,
717
+ description: "The proxy command to run",
718
+ })
719
+ .options({
720
+ local: {
721
+ type: "boolean",
722
+ default: true,
723
+ description: "Run on my machine",
724
+ },
725
+ port: {
726
+ type: "number",
727
+ default: 8788,
728
+ description: "The port to listen on (serve from)",
729
+ },
730
+ proxy: {
731
+ type: "number",
732
+ description:
733
+ "The port to proxy (where the static assets are served)",
734
+ },
735
+ "script-path": {
736
+ type: "string",
737
+ default: "_worker.js",
738
+ description:
739
+ "The location of the single Worker script if not using functions",
740
+ },
741
+ binding: {
742
+ type: "array",
743
+ description: "Bind variable/secret (KEY=VALUE)",
744
+ alias: "b",
745
+ },
746
+ kv: {
747
+ type: "array",
748
+ description: "KV namespace to bind",
749
+ alias: "k",
750
+ },
751
+ do: {
752
+ type: "array",
753
+ description: "Durable Object to bind (NAME=CLASS)",
754
+ alias: "o",
755
+ },
756
+ "live-reload": {
757
+ type: "boolean",
758
+ default: false,
759
+ description: "Auto reload HTML pages when change is detected",
760
+ },
761
+ // TODO: Miniflare user options
762
+ });
763
+ },
764
+ async ({
765
+ local,
766
+ directory,
767
+ port,
768
+ proxy: requestedProxyPort,
769
+ "script-path": singleWorkerScriptPath,
770
+ binding: bindings = [],
771
+ kv: kvs = [],
772
+ do: durableObjects = [],
773
+ "live-reload": liveReload,
774
+ _: [_pages, _dev, ...remaining],
775
+ }) => {
776
+ if (!local) {
777
+ console.error("Only local mode is supported at the moment.");
778
+ return;
779
+ }
682
780
 
683
- process.on("SIGINT", () => exit());
684
- process.on("SIGTERM", () => exit());
685
- }
781
+ const functionsDirectory = "./functions";
782
+ const usingFunctions = existsSync(functionsDirectory);
686
783
 
687
- let miniflareArgs: MiniflareOptions = {};
784
+ const command = remaining as (string | number)[];
688
785
 
689
- if (usingFunctions) {
690
- const scriptPath = join(tmpdir(), "./functionsWorker.js");
691
- miniflareArgs = {
692
- scriptPath,
693
- buildWatchPaths: [functionsDirectory],
694
- buildCommand: `npx @cloudflare/pages-functions-compiler build ${functionsDirectory} --outfile ${scriptPath}`,
695
- };
696
- } else {
697
- const scriptPath =
698
- directory !== undefined
699
- ? join(directory, singleWorkerScriptPath)
700
- : singleWorkerScriptPath;
786
+ let proxyPort: number | void;
701
787
 
702
- if (!existsSync(scriptPath)) {
703
- return exit(
704
- `No Worker script found at ${scriptPath}. Please either create a functions directory or create a single Worker at ${scriptPath}.`
705
- );
788
+ if (directory === undefined) {
789
+ proxyPort = await spawnProxyProcess({
790
+ port: requestedProxyPort,
791
+ command,
792
+ });
793
+ if (proxyPort === undefined) return undefined;
706
794
  }
707
795
 
708
- miniflareArgs = {
709
- scriptPath,
710
- };
711
- }
796
+ let miniflareArgs: MiniflareOptions = {};
712
797
 
713
- const { Miniflare, Log, LogLevel } = await import("miniflare");
714
- const { fetch } = await import("@miniflare/core");
798
+ let scriptReadyResolve: () => void;
799
+ const scriptReadyPromise = new Promise<void>(
800
+ (resolve) => (scriptReadyResolve = resolve)
801
+ );
715
802
 
716
- class MiniflareLogger extends Log {
717
- logWithLevel(level: number, message: string) {
718
- if (level <= this.level) {
719
- switch (level) {
720
- case LogLevel.NONE: {
721
- break;
722
- }
723
- case LogLevel.ERROR: {
724
- console.error(message);
725
- break;
726
- }
727
- case LogLevel.WARN: {
728
- console.warn(message);
729
- break;
730
- }
731
- case LogLevel.INFO: {
732
- console.info(message);
733
- break;
734
- }
735
- case LogLevel.DEBUG: {
736
- console.debug(message);
737
- break;
738
- }
739
- case LogLevel.VERBOSE: {
740
- console.debug(message);
741
- break;
803
+ if (usingFunctions) {
804
+ const scriptPath = join(tmpdir(), "./functionsWorker.js");
805
+
806
+ console.log(`Compiling worker to "${scriptPath}"...`);
807
+
808
+ await buildFunctions({
809
+ scriptPath,
810
+ functionsDirectory,
811
+ sourcemap: true,
812
+ watch: true,
813
+ onEnd: () => scriptReadyResolve(),
814
+ });
815
+
816
+ watch([functionsDirectory], {
817
+ persistent: true,
818
+ ignoreInitial: true,
819
+ }).on("all", async () => {
820
+ await buildFunctions({
821
+ scriptPath,
822
+ functionsDirectory,
823
+ sourcemap: true,
824
+ watch: true,
825
+ onEnd: () => scriptReadyResolve(),
826
+ });
827
+ });
828
+
829
+ miniflareArgs = {
830
+ scriptPath,
831
+ };
832
+ } else {
833
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
834
+ scriptReadyResolve!();
835
+
836
+ const scriptPath =
837
+ directory !== undefined
838
+ ? join(directory, singleWorkerScriptPath)
839
+ : singleWorkerScriptPath;
840
+
841
+ if (existsSync(scriptPath)) {
842
+ miniflareArgs = {
843
+ scriptPath,
844
+ };
845
+ } else {
846
+ console.log("No functions. Shimming...");
847
+ miniflareArgs = {
848
+ // TODO: The fact that these request/response hacks are necessary is ridiculous.
849
+ // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
850
+ script: `
851
+ export default {
852
+ async fetch(request, env, context) {
853
+ const response = await env.ASSETS.fetch(request.url, request)
854
+ return new Response(response.body, response)
742
855
  }
743
- }
856
+ }`,
857
+ };
744
858
  }
745
859
  }
746
860
 
747
- log(message: string) {}
861
+ // Defer importing miniflare until we really need it
862
+ const { Miniflare, Log, LogLevel } = await import("miniflare");
863
+ const { Response, fetch } = await import("@miniflare/core");
748
864
 
749
- error(message: Error) {
750
- this.logWithLevel(
751
- LogLevel.ERROR,
752
- message.stack ? message.stack : `${message}`
753
- );
754
- }
755
- }
865
+ // Wait for esbuild to finish building before starting Miniflare.
866
+ // This must be before the call to `new Miniflare`, as that will
867
+ // asynchronously start loading the script. `await startServer()`
868
+ // internally just waits for that promise to resolve.
869
+ await scriptReadyPromise;
756
870
 
757
- const miniflare = new Miniflare({
758
- port,
759
- watch: true,
760
- modules: true,
871
+ // `assetsFetch()` will only be called if there is `proxyPort` defined.
872
+ // We only define `proxyPort`, above, when there is no `directory` defined.
873
+ const assetsFetch =
874
+ directory !== undefined
875
+ ? await generateAssetsFetch(directory)
876
+ : invalidAssetsFetch;
761
877
 
762
- log: new MiniflareLogger(LogLevel.ERROR),
878
+ const miniflare = new Miniflare({
879
+ port,
880
+ watch: true,
881
+ modules: true,
763
882
 
764
- kvNamespaces: kvs.map((kv) => kv.toString()),
883
+ log: new Log(LogLevel.ERROR, { prefix: "pages" }),
884
+ logUnhandledRejections: true,
885
+ sourceMap: true,
765
886
 
766
- durableObjects: Object.fromEntries(
767
- durableObjects.map((durableObject) =>
768
- durableObject.toString().split("=")
769
- )
770
- ),
887
+ kvNamespaces: kvs.map((kv) => kv.toString()),
771
888
 
772
- bindings: {
773
- // User bindings
774
- ...Object.fromEntries(
775
- bindings.map((binding) => binding.toString().split("="))
889
+ durableObjects: Object.fromEntries(
890
+ durableObjects.map((durableObject) =>
891
+ durableObject.toString().split("=")
892
+ )
776
893
  ),
777
894
 
895
+ // User bindings
896
+ bindings: {
897
+ ...Object.fromEntries(
898
+ bindings.map((binding) => binding.toString().split("=", 1))
899
+ ),
900
+ },
901
+
778
902
  // env.ASSETS.fetch
779
- ASSETS: {
780
- fetch: async (
781
- input: RequestInfo,
782
- init?: RequestInit | undefined
783
- ) => {
903
+ serviceBindings: {
904
+ async ASSETS(request: Request) {
784
905
  if (proxyPort) {
785
906
  try {
786
- let request = new Request(input as any, init);
787
907
  const url = new URL(request.url);
788
- url.host = `127.0.0.1:${proxyPort}`;
789
- request = new Request(url.toString(), request);
790
- return await fetch(request);
908
+ url.host = `localhost:${proxyPort}`;
909
+ return await fetch(url, request);
791
910
  } catch (thrown) {
792
911
  console.error(`Could not proxy request: ${thrown}`);
793
912
 
@@ -799,9 +918,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
799
918
  }
800
919
  } else {
801
920
  try {
802
- return await (
803
- await generateAssetsFetch(directory)
804
- )(input as any, init as any);
921
+ return await assetsFetch(request);
805
922
  } catch (thrown) {
806
923
  console.error(`Could not serve static asset: ${thrown}`);
807
924
 
@@ -814,30 +931,115 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
814
931
  }
815
932
  },
816
933
  },
817
- },
818
934
 
819
- kvPersist: true,
820
- durableObjectsPersist: true,
821
- cachePersist: true,
935
+ kvPersist: true,
936
+ durableObjectsPersist: true,
937
+ cachePersist: true,
938
+ liveReload,
822
939
 
823
- ...miniflareArgs,
824
- });
940
+ ...miniflareArgs,
941
+ });
942
+
943
+ try {
944
+ // `startServer` might throw if user code contains errors
945
+ const server = await miniflare.startServer();
946
+ console.log(`Serving at http://localhost:${port}/`);
825
947
 
826
- const server = await miniflare.startServer();
827
- console.log(`Serving at http://127.0.0.1:${port}/`);
948
+ if (process.env.BROWSER !== "none") {
949
+ await openInBrowser(`http://localhost:${port}/`);
950
+ }
828
951
 
829
- if (process.env.BROWSER !== "none") {
830
- open(`http://127.0.0.1:${port}/`);
952
+ if (directory !== undefined && liveReload) {
953
+ watch([directory], {
954
+ persistent: true,
955
+ ignoreInitial: true,
956
+ }).on("all", async () => {
957
+ await miniflare.reload();
958
+ });
959
+ }
960
+
961
+ EXIT_CALLBACKS.push(() => {
962
+ server.close();
963
+ miniflare.dispose().catch((err) => miniflare.log.error(err));
964
+ });
965
+ } catch (e) {
966
+ miniflare.log.error(e as Error);
967
+ EXIT("Could not start Miniflare.", 1);
968
+ }
831
969
  }
970
+ )
971
+ .command("functions", false, (yargs) =>
972
+ // we hide this command from help output because
973
+ // it's not meant to be used directly right now
974
+ yargs.command(
975
+ "build [directory]",
976
+ "Compile a folder of Cloudflare Pages Functions into a single Worker",
977
+ (yargs) =>
978
+ yargs
979
+ .positional("directory", {
980
+ type: "string",
981
+ default: "functions",
982
+ description: "The directory of Pages Functions",
983
+ })
984
+ .options({
985
+ "script-path": {
986
+ type: "string",
987
+ default: "_worker.js",
988
+ description: "The location of the output Worker script",
989
+ },
990
+ "output-config-path": {
991
+ type: "string",
992
+ description: "The location for the output config file",
993
+ },
994
+ minify: {
995
+ type: "boolean",
996
+ default: false,
997
+ description: "Minify the output Worker script",
998
+ },
999
+ sourcemap: {
1000
+ type: "boolean",
1001
+ default: false,
1002
+ description:
1003
+ "Generate a sourcemap for the output Worker script",
1004
+ },
1005
+ "fallback-service": {
1006
+ type: "string",
1007
+ default: "ASSETS",
1008
+ description:
1009
+ "The service to fallback to at the end of the `next` chain. Setting to '' will fallback to the global `fetch`.",
1010
+ },
1011
+ watch: {
1012
+ type: "boolean",
1013
+ default: false,
1014
+ description:
1015
+ "Watch for changes to the functions and automatically rebuild the Worker script",
1016
+ },
1017
+ }),
1018
+ async ({
1019
+ directory,
1020
+ "script-path": scriptPath,
1021
+ "output-config-path": outputConfigPath,
1022
+ minify,
1023
+ sourcemap,
1024
+ fallbackService,
1025
+ watch,
1026
+ }) => {
1027
+ await buildFunctions({
1028
+ scriptPath,
1029
+ outputConfigPath,
1030
+ functionsDirectory: directory,
1031
+ minify,
1032
+ sourcemap,
1033
+ fallbackService,
1034
+ watch,
1035
+ });
1036
+ }
1037
+ )
1038
+ );
1039
+ };
832
1040
 
833
- process.on("SIGINT", () => {
834
- server.close();
835
- miniflare.dispose();
836
- });
837
- process.on("SIGTERM", () => {
838
- server.close();
839
- miniflare.dispose();
840
- });
841
- }
1041
+ const invalidAssetsFetch: typeof fetch = () => {
1042
+ throw new Error(
1043
+ "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
842
1044
  );
843
1045
  };