wrangler 0.0.0-e0d2f35 → 0.0.0-e1b8ac4

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 (160) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +48 -4
  4. package/kv-asset-handler.js +1 -0
  5. package/miniflare-dist/index.mjs +4886 -0
  6. package/package.json +58 -31
  7. package/pages/functions/buildPlugin.ts +105 -0
  8. package/pages/functions/buildWorker.ts +93 -4
  9. package/pages/functions/filepath-routing.test.ts +223 -28
  10. package/pages/functions/filepath-routing.ts +92 -131
  11. package/pages/functions/routes.ts +16 -18
  12. package/pages/functions/template-plugin.ts +147 -0
  13. package/pages/functions/template-worker.ts +24 -17
  14. package/src/__tests__/config-cache.test.ts +36 -0
  15. package/src/__tests__/configuration.test.ts +3548 -0
  16. package/src/__tests__/dev.test.tsx +1049 -65
  17. package/src/__tests__/guess-worker-format.test.ts +85 -0
  18. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  19. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  20. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  21. package/src/__tests__/helpers/mock-account-id.ts +48 -0
  22. package/src/__tests__/helpers/mock-bin.ts +36 -0
  23. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +50 -12
  24. package/src/__tests__/helpers/mock-console.ts +87 -0
  25. package/src/__tests__/helpers/mock-dialogs.ts +153 -0
  26. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  27. package/src/__tests__/helpers/mock-istty.ts +27 -0
  28. package/src/__tests__/{mock-kv.ts → helpers/mock-kv.ts} +9 -16
  29. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  30. package/src/__tests__/helpers/mock-process.ts +24 -0
  31. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  32. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  33. package/src/__tests__/{run-in-tmp.ts → helpers/run-in-tmp.ts} +11 -1
  34. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  35. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  36. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  37. package/src/__tests__/https-options.test.ts +130 -0
  38. package/src/__tests__/index.test.ts +171 -338
  39. package/src/__tests__/init.test.ts +1992 -0
  40. package/src/__tests__/jest.setup.ts +86 -8
  41. package/src/__tests__/kv.test.ts +745 -449
  42. package/src/__tests__/logger.test.ts +141 -0
  43. package/src/__tests__/package-manager.test.ts +353 -0
  44. package/src/__tests__/pages.test.ts +754 -0
  45. package/src/__tests__/parse.test.ts +330 -0
  46. package/src/__tests__/publish.test.ts +5631 -903
  47. package/src/__tests__/r2.test.ts +233 -0
  48. package/src/__tests__/route.test.ts +45 -0
  49. package/src/__tests__/secret.test.ts +370 -95
  50. package/src/__tests__/tail.test.ts +690 -0
  51. package/src/__tests__/user.test.ts +132 -0
  52. package/src/__tests__/whoami.test.tsx +58 -20
  53. package/src/abort.d.ts +3 -0
  54. package/src/bundle.ts +204 -0
  55. package/src/cfetch/index.ts +30 -20
  56. package/src/cfetch/internal.ts +101 -30
  57. package/src/cli.ts +6 -2
  58. package/src/config/README.md +107 -0
  59. package/src/config/config.ts +243 -0
  60. package/src/config/diagnostics.ts +80 -0
  61. package/src/config/environment.ts +420 -0
  62. package/src/config/index.ts +225 -0
  63. package/src/config/validation-helpers.ts +584 -0
  64. package/src/config/validation.ts +1580 -0
  65. package/src/config-cache.ts +45 -0
  66. package/src/create-worker-preview.ts +185 -0
  67. package/src/create-worker-upload-form.ts +254 -0
  68. package/src/dev/dev-vars.ts +36 -0
  69. package/src/dev/dev.tsx +399 -0
  70. package/src/dev/local.tsx +321 -0
  71. package/src/dev/remote.tsx +242 -0
  72. package/src/dev/use-esbuild.ts +112 -0
  73. package/src/dialogs.tsx +66 -8
  74. package/src/durable.ts +102 -0
  75. package/src/entry.ts +322 -0
  76. package/src/environment-variables.ts +35 -0
  77. package/src/errors.ts +11 -0
  78. package/src/generate-auth-url.ts +33 -0
  79. package/src/generate-random-state.ts +16 -0
  80. package/src/https-options.ts +123 -0
  81. package/src/index.tsx +1848 -1019
  82. package/src/inspect.ts +87 -20
  83. package/src/intl-polyfill.d.ts +139 -0
  84. package/src/{kv.tsx → kv.ts} +199 -67
  85. package/src/logger.ts +73 -0
  86. package/src/miniflare-cli/README.md +30 -0
  87. package/src/miniflare-cli/enum-keys.ts +17 -0
  88. package/src/miniflare-cli/index.ts +41 -0
  89. package/src/miniflare-cli/request-context.ts +40 -0
  90. package/src/module-collection.ts +229 -34
  91. package/src/open-in-browser.ts +17 -0
  92. package/src/package-manager.ts +174 -0
  93. package/src/pages.tsx +1145 -95
  94. package/src/parse.ts +222 -0
  95. package/src/paths.ts +26 -0
  96. package/src/proxy.ts +180 -74
  97. package/src/publish.ts +626 -310
  98. package/src/r2.ts +50 -0
  99. package/src/selfsigned.d.ts +29 -0
  100. package/src/sites.tsx +187 -59
  101. package/src/tail/filters.ts +279 -0
  102. package/src/tail/index.ts +299 -0
  103. package/src/tail/printing.ts +67 -0
  104. package/src/update-check.ts +19 -0
  105. package/src/user.tsx +359 -176
  106. package/src/whoami.tsx +5 -3
  107. package/src/{api/worker.ts → worker.ts} +66 -40
  108. package/src/zones.ts +73 -0
  109. package/templates/gitignore +171 -0
  110. package/templates/new-worker-scheduled.js +17 -0
  111. package/templates/new-worker-scheduled.ts +32 -0
  112. package/templates/new-worker.js +15 -0
  113. package/templates/new-worker.ts +30 -0
  114. package/{static-asset-facade.js → templates/static-asset-facade.js} +3 -7
  115. package/templates/tsconfig.json +105 -0
  116. package/wrangler-dist/cli.js +112889 -110835
  117. package/src/__tests__/logout.test.ts +0 -64
  118. package/src/__tests__/mock-console.ts +0 -34
  119. package/src/__tests__/mock-dialogs.ts +0 -65
  120. package/src/__tests__/mock-user.ts +0 -27
  121. package/src/__tests__/run-wrangler.ts +0 -8
  122. package/src/api/form_data.ts +0 -135
  123. package/src/api/preview.ts +0 -128
  124. package/src/config.ts +0 -641
  125. package/src/dev.tsx +0 -830
  126. package/src/tail.tsx +0 -73
  127. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  128. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  129. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  130. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  131. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  132. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  133. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  134. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  135. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  136. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  137. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  138. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  139. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  140. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  141. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  142. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  143. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  144. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  145. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  146. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  147. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  148. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  149. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  150. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  151. package/vendor/wrangler-mime/LICENSE +0 -21
  152. package/vendor/wrangler-mime/Mime.js +0 -97
  153. package/vendor/wrangler-mime/README.md +0 -187
  154. package/vendor/wrangler-mime/cli.js +0 -46
  155. package/vendor/wrangler-mime/index.js +0 -4
  156. package/vendor/wrangler-mime/lite.js +0 -4
  157. package/vendor/wrangler-mime/package.json +0 -52
  158. package/vendor/wrangler-mime/types/other.js +0 -1
  159. package/vendor/wrangler-mime/types/standard.js +0 -1
  160. package/wrangler-dist/cli.js.map +0 -7
package/src/pages.tsx CHANGED
@@ -1,36 +1,119 @@
1
1
  /* eslint-disable no-shadow */
2
2
 
3
- import type { BuilderCallback } from "yargs";
4
- import { join } from "path";
5
- import { tmpdir } from "os";
6
- import { existsSync, lstatSync, readFileSync, writeFileSync } from "fs";
7
- import { execSync, spawn } from "child_process";
8
- import { URL } from "url";
9
- import { getType } from "mime";
10
- import open from "open";
3
+ import { execSync, spawn } from "node:child_process";
4
+ import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { readdir, readFile, stat } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { dirname, join, sep, extname, basename } from "node:path";
8
+ import { cwd } from "node:process";
9
+ import { URL } from "node:url";
10
+ import { hash } from "blake3-wasm";
11
11
  import { watch } from "chokidar";
12
- import type { BuildResult } from "esbuild";
12
+ import { render, Text } from "ink";
13
+ import SelectInput from "ink-select-input";
14
+ import Spinner from "ink-spinner";
15
+ import Table from "ink-table";
16
+ import { getType } from "mime";
17
+ import PQueue from "p-queue";
18
+ import prettyBytes from "pretty-bytes";
19
+ import React from "react";
20
+ import { format as timeagoFormat } from "timeago.js";
21
+ import { File, FormData } from "undici";
22
+ import { buildPlugin } from "../pages/functions/buildPlugin";
13
23
  import { buildWorker } from "../pages/functions/buildWorker";
14
- import type { Config } from "../pages/functions/routes";
15
- import { writeRoutesModule } from "../pages/functions/routes";
16
24
  import { generateConfigFromFileTree } from "../pages/functions/filepath-routing";
25
+ import { writeRoutesModule } from "../pages/functions/routes";
26
+ import { fetchResult } from "./cfetch";
27
+ import { getConfigCache, saveToConfigCache } from "./config-cache";
28
+ import { prompt } from "./dialogs";
29
+ import { FatalError } from "./errors";
30
+ import { logger } from "./logger";
31
+ import { getRequestContextCheckOptions } from "./miniflare-cli/request-context";
32
+ import openInBrowser from "./open-in-browser";
33
+ import { toUrlPath } from "./paths";
34
+ import { requireAuth } from "./user";
35
+ import type { Config } from "../pages/functions/routes";
36
+ import type {
37
+ Headers as MiniflareHeaders,
38
+ Request as MiniflareRequest,
39
+ fetch as miniflareFetch,
40
+ } from "@miniflare/core";
41
+ import type { BuildResult } from "esbuild";
42
+ import type { MiniflareOptions } from "miniflare";
43
+ import type { BuilderCallback, CommandModule } from "yargs";
44
+
45
+ export type Project = {
46
+ name: string;
47
+ subdomain: string;
48
+ domains: Array<string>;
49
+ source?: {
50
+ type: string;
51
+ };
52
+ latest_deployment?: {
53
+ modified_on: string;
54
+ };
55
+ created_on: string;
56
+ production_branch: string;
57
+ };
58
+
59
+ export type Deployment = {
60
+ id: string;
61
+ environment: string;
62
+ deployment_trigger: {
63
+ metadata: {
64
+ commit_hash: string;
65
+ branch: string;
66
+ };
67
+ };
68
+ url: string;
69
+ latest_stage: {
70
+ status: string;
71
+ ended_on: string;
72
+ };
73
+ project_name: string;
74
+ };
75
+
76
+ export type UploadPayloadFile = {
77
+ key: string;
78
+ value: string;
79
+ metadata: { contentType: string };
80
+ base64: boolean;
81
+ };
82
+
83
+ interface PagesConfigCache {
84
+ account_id?: string;
85
+ project_name?: string;
86
+ }
87
+
88
+ const PAGES_CONFIG_CACHE_FILENAME = "pages.json";
89
+ const MAX_BUCKET_SIZE = 50 * 1024 * 1024;
90
+ const MAX_BUCKET_FILE_COUNT = 5000;
91
+ const BULK_UPLOAD_CONCURRENCY = 3;
92
+ const MAX_UPLOAD_ATTEMPTS = 5;
17
93
 
18
94
  // Defer importing miniflare until we really need it. This takes ~0.5s
19
95
  // and also modifies some `stream/web` and `undici` prototypes, so we
20
96
  // don't want to do this if pages commands aren't being called.
21
- import type { Headers, Request, fetch } from "@miniflare/core";
22
- import type { MiniflareOptions } from "miniflare";
23
97
 
24
- const EXIT_CALLBACKS: (() => void)[] = [];
25
- const EXIT = (message?: string, code?: number) => {
26
- if (message) console.log(message);
27
- if (code) process.exitCode = code;
28
- EXIT_CALLBACKS.forEach((callback) => callback());
29
- process.exit(code);
98
+ export const pagesBetaWarning =
99
+ "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose";
100
+
101
+ const isInPagesCI = !!process.env.CF_PAGES;
102
+
103
+ const CLEANUP_CALLBACKS: (() => void)[] = [];
104
+ const CLEANUP = () => {
105
+ CLEANUP_CALLBACKS.forEach((callback) => callback());
106
+ RUNNING_BUILDERS.forEach((builder) => builder.stop?.());
30
107
  };
31
108
 
32
- process.on("SIGINT", () => EXIT());
33
- process.on("SIGTERM", () => EXIT());
109
+ process.on("SIGINT", () => {
110
+ CLEANUP();
111
+ process.exit();
112
+ });
113
+ process.on("SIGTERM", () => {
114
+ CLEANUP();
115
+ process.exit();
116
+ });
34
117
 
35
118
  function isWindows() {
36
119
  return process.platform === "win32";
@@ -90,7 +173,7 @@ function getPort(pid: number) {
90
173
  const match = matches[0];
91
174
  if (match) return parseInt(match[1]);
92
175
  } catch (thrown) {
93
- console.error(
176
+ logger.error(
94
177
  `Error scanning for ports of process with PID ${pid}: ${thrown}`
95
178
  );
96
179
  }
@@ -103,13 +186,15 @@ async function spawnProxyProcess({
103
186
  port?: number;
104
187
  command: (string | number)[];
105
188
  }): Promise<void | number> {
106
- if (command.length === 0)
107
- return EXIT(
189
+ if (command.length === 0) {
190
+ CLEANUP();
191
+ throw new FatalError(
108
192
  "Must specify a directory of static assets to serve or a command to run.",
109
193
  1
110
194
  );
195
+ }
111
196
 
112
- console.log(`Running ${command.join(" ")}...`);
197
+ logger.log(`Running ${command.join(" ")}...`);
113
198
  const proxy = spawn(
114
199
  command[0].toString(),
115
200
  command.slice(1).map((value) => value.toString()),
@@ -121,30 +206,30 @@ async function spawnProxyProcess({
121
206
  },
122
207
  }
123
208
  );
124
- EXIT_CALLBACKS.push(() => {
209
+ CLEANUP_CALLBACKS.push(() => {
125
210
  proxy.kill();
126
211
  });
127
212
 
128
213
  proxy.stdout.on("data", (data) => {
129
- console.log(`[proxy]: ${data}`);
214
+ logger.log(`[proxy]: ${data}`);
130
215
  });
131
216
 
132
217
  proxy.stderr.on("data", (data) => {
133
- console.error(`[proxy]: ${data}`);
218
+ logger.error(`[proxy]: ${data}`);
134
219
  });
135
220
 
136
221
  proxy.on("close", (code) => {
137
- console.error(`Proxy exited with status ${code}.`);
222
+ logger.error(`Proxy exited with status ${code}.`);
138
223
  });
139
224
 
140
225
  // Wait for proxy process to start...
141
226
  while (!proxy.pid) {}
142
227
 
143
228
  if (port === undefined) {
144
- console.log(
229
+ logger.log(
145
230
  `Sleeping ${SECONDS_TO_WAIT_FOR_PROXY} seconds to allow proxy process to start before attempting to automatically determine port...`
146
231
  );
147
- console.log("To skip, specify the proxy port with --proxy.");
232
+ logger.log("To skip, specify the proxy port with --proxy.");
148
233
  await sleep(SECONDS_TO_WAIT_FOR_PROXY * 1000);
149
234
 
150
235
  port = getPids(proxy.pid)
@@ -152,12 +237,13 @@ async function spawnProxyProcess({
152
237
  .filter((port) => port !== undefined)[0];
153
238
 
154
239
  if (port === undefined) {
155
- return EXIT(
240
+ CLEANUP();
241
+ throw new FatalError(
156
242
  "Could not automatically determine proxy port. Please specify the proxy port with --proxy.",
157
243
  1
158
244
  );
159
245
  } else {
160
- console.log(`Automatically determined the proxy port to be ${port}.`);
246
+ logger.log(`Automatically determined the proxy port to be ${port}.`);
161
247
  }
162
248
  }
163
249
 
@@ -214,7 +300,7 @@ function generateRulesMatcher<T>(
214
300
  T
215
301
  ][];
216
302
 
217
- return ({ request }: { request: Request }) => {
303
+ return ({ request }: { request: MiniflareRequest }) => {
218
304
  const { pathname, host } = new URL(request.url);
219
305
 
220
306
  return compiledRules
@@ -287,7 +373,7 @@ function generateHeadersMatcher(headersFile: string) {
287
373
  )
288
374
  );
289
375
 
290
- return (request: Request) => {
376
+ return (request: MiniflareRequest) => {
291
377
  const matches = rulesMatcher({
292
378
  request,
293
379
  });
@@ -336,7 +422,7 @@ function generateRedirectsMatcher(redirectsFile: string) {
336
422
  })
337
423
  );
338
424
 
339
- return (request: Request) => {
425
+ return (request: MiniflareRequest) => {
340
426
  const match = rulesMatcher({
341
427
  request,
342
428
  })[0];
@@ -391,7 +477,9 @@ function hasFileExtension(pathname: string) {
391
477
  return /\/.+\.[a-z0-9]+$/i.test(pathname);
392
478
  }
393
479
 
394
- async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
480
+ async function generateAssetsFetch(
481
+ directory: string
482
+ ): Promise<typeof miniflareFetch> {
395
483
  // Defer importing miniflare until we really need it
396
484
  const { Headers, Request, Response } = await import("@miniflare/core");
397
485
 
@@ -424,12 +512,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
424
512
  }).on("change", (path) => {
425
513
  switch (path) {
426
514
  case headersFile: {
427
- console.log("_headers modified. Re-evaluating...");
515
+ logger.log("_headers modified. Re-evaluating...");
428
516
  headersMatcher = generateHeadersMatcher(headersFile);
429
517
  break;
430
518
  }
431
519
  case redirectsFile: {
432
- console.log("_redirects modified. Re-evaluating...");
520
+ logger.log("_redirects modified. Re-evaluating...");
433
521
  redirectsMatcher = generateRedirectsMatcher(redirectsFile);
434
522
  break;
435
523
  }
@@ -440,12 +528,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
440
528
  return readFileSync(file);
441
529
  };
442
530
 
443
- const generateResponse = (request: Request) => {
531
+ const generateResponse = (request: MiniflareRequest) => {
444
532
  const url = new URL(request.url);
445
533
 
446
534
  const deconstructedResponse: {
447
535
  status: number;
448
- headers: Headers;
536
+ headers: MiniflareHeaders;
449
537
  body?: Buffer;
450
538
  } = {
451
539
  status: 200,
@@ -597,8 +685,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
597
685
  };
598
686
 
599
687
  const attachHeaders = (
600
- request: Request,
601
- deconstructedResponse: { status: number; headers: Headers; body?: Buffer }
688
+ request: MiniflareRequest,
689
+ deconstructedResponse: {
690
+ status: number;
691
+ headers: MiniflareHeaders;
692
+ body?: Buffer;
693
+ }
602
694
  ) => {
603
695
  const headers = deconstructedResponse.headers;
604
696
  const newHeaders = new Headers({});
@@ -642,7 +734,7 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
642
734
  const RUNNING_BUILDERS: BuildResult[] = [];
643
735
 
644
736
  async function buildFunctions({
645
- scriptPath,
737
+ outfile,
646
738
  outputConfigPath,
647
739
  functionsDirectory,
648
740
  minify = false,
@@ -650,8 +742,11 @@ async function buildFunctions({
650
742
  fallbackService = "ASSETS",
651
743
  watch = false,
652
744
  onEnd,
745
+ plugin = false,
746
+ buildOutputDirectory,
747
+ nodeCompat,
653
748
  }: {
654
- scriptPath: string;
749
+ outfile: string;
655
750
  outputConfigPath?: string;
656
751
  functionsDirectory: string;
657
752
  minify?: boolean;
@@ -659,22 +754,26 @@ async function buildFunctions({
659
754
  fallbackService?: string;
660
755
  watch?: boolean;
661
756
  onEnd?: () => void;
757
+ plugin?: boolean;
758
+ buildOutputDirectory?: string;
759
+ nodeCompat?: boolean;
662
760
  }) {
663
761
  RUNNING_BUILDERS.forEach(
664
762
  (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
665
763
  );
666
764
 
667
- const routesModule = join(tmpdir(), "./functionsRoutes.mjs");
765
+ const routesModule = join(tmpdir(), `./functionsRoutes-${Math.random()}.mjs`);
766
+ const baseURL = toUrlPath("/");
668
767
 
669
768
  const config: Config = await generateConfigFromFileTree({
670
769
  baseDir: functionsDirectory,
671
- baseURL: "/",
770
+ baseURL,
672
771
  });
673
772
 
674
773
  if (outputConfigPath) {
675
774
  writeFileSync(
676
775
  outputConfigPath,
677
- JSON.stringify({ ...config, baseURL: "/" }, null, 2)
776
+ JSON.stringify({ ...config, baseURL }, null, 2)
678
777
  );
679
778
  }
680
779
 
@@ -684,23 +783,644 @@ async function buildFunctions({
684
783
  outfile: routesModule,
685
784
  });
686
785
 
687
- RUNNING_BUILDERS.push(
688
- await buildWorker({
689
- routesModule,
690
- outfile: scriptPath,
691
- minify,
692
- sourcemap,
693
- fallbackService,
694
- watch,
695
- onEnd,
696
- })
697
- );
786
+ if (plugin) {
787
+ RUNNING_BUILDERS.push(
788
+ await buildPlugin({
789
+ routesModule,
790
+ outfile,
791
+ minify,
792
+ sourcemap,
793
+ watch,
794
+ nodeCompat,
795
+ onEnd,
796
+ })
797
+ );
798
+ } else {
799
+ RUNNING_BUILDERS.push(
800
+ await buildWorker({
801
+ routesModule,
802
+ outfile,
803
+ minify,
804
+ sourcemap,
805
+ fallbackService,
806
+ watch,
807
+ onEnd,
808
+ buildOutputDirectory,
809
+ nodeCompat,
810
+ })
811
+ );
812
+ }
813
+ }
814
+
815
+ interface CreateDeploymentArgs {
816
+ directory: string;
817
+ projectName?: string;
818
+ branch?: string;
819
+ commitHash?: string;
820
+ commitMessage?: string;
821
+ commitDirty?: boolean;
698
822
  }
699
823
 
824
+ const upload = async ({
825
+ directory,
826
+ accountId,
827
+ projectName,
828
+ }: {
829
+ directory: string;
830
+ accountId: string;
831
+ projectName: string;
832
+ }) => {
833
+ type FileContainer = {
834
+ content: string;
835
+ contentType: string;
836
+ sizeInBytes: number;
837
+ hash: string;
838
+ };
839
+
840
+ const IGNORE_LIST = [
841
+ "_worker.js",
842
+ "_redirects",
843
+ "_headers",
844
+ ".DS_Store",
845
+ "node_modules",
846
+ ".git",
847
+ ];
848
+
849
+ const walk = async (
850
+ dir: string,
851
+ fileMap: Map<string, FileContainer> = new Map(),
852
+ depth = 0
853
+ ) => {
854
+ const files = await readdir(dir);
855
+
856
+ await Promise.all(
857
+ files.map(async (file) => {
858
+ const filepath = join(dir, file);
859
+ const filestat = await stat(filepath);
860
+
861
+ if (IGNORE_LIST.includes(file)) {
862
+ return;
863
+ }
864
+
865
+ if (filestat.isSymbolicLink()) {
866
+ return;
867
+ }
868
+
869
+ if (filestat.isDirectory()) {
870
+ fileMap = await walk(filepath, fileMap, depth + 1);
871
+ } else {
872
+ let name;
873
+ if (depth) {
874
+ name = filepath.split(sep).slice(1).join("/");
875
+ } else {
876
+ name = file;
877
+ }
878
+
879
+ // TODO: Move this to later so we don't hold as much in memory
880
+ const fileContent = await readFile(filepath);
881
+
882
+ const base64Content = fileContent.toString("base64");
883
+ const extension = extname(basename(name)).substring(1);
884
+
885
+ if (filestat.size > 25 * 1024 * 1024) {
886
+ throw new Error(
887
+ `Error: Pages only supports files up to ${prettyBytes(
888
+ 25 * 1024 * 1024
889
+ )} in size\n${name} is ${prettyBytes(filestat.size)} in size`
890
+ );
891
+ }
892
+
893
+ fileMap.set(name, {
894
+ content: base64Content,
895
+ contentType: getType(name) || "application/octet-stream",
896
+ sizeInBytes: filestat.size,
897
+ hash: hash(base64Content + extension)
898
+ .toString("hex")
899
+ .slice(0, 32),
900
+ });
901
+ }
902
+ })
903
+ );
904
+
905
+ return fileMap;
906
+ };
907
+
908
+ const fileMap = await walk(directory);
909
+
910
+ if (fileMap.size > 20000) {
911
+ throw new FatalError(
912
+ `Error: Pages only supports up to 20,000 files in a deployment. Ensure you have specified your build output directory correctly.`,
913
+ 1
914
+ );
915
+ }
916
+
917
+ const files = [...fileMap.values()];
918
+
919
+ async function fetchJwt(): Promise<string> {
920
+ return (
921
+ await fetchResult<{ jwt: string }>(
922
+ `/accounts/${accountId}/pages/projects/${projectName}/upload-token`
923
+ )
924
+ ).jwt;
925
+ }
926
+
927
+ let jwt = await fetchJwt();
928
+
929
+ const start = Date.now();
930
+
931
+ const missingHashes = await fetchResult<string[]>(
932
+ `/pages/assets/check-missing`,
933
+ {
934
+ method: "POST",
935
+ headers: {
936
+ "Content-Type": "application/json",
937
+ Authorization: `Bearer ${jwt}`,
938
+ },
939
+ body: JSON.stringify({
940
+ hashes: files.map(({ hash }) => hash),
941
+ }),
942
+ }
943
+ );
944
+
945
+ const sortedFiles = files
946
+ .filter((file) => missingHashes.includes(file.hash))
947
+ .sort((a, b) => b.sizeInBytes - a.sizeInBytes);
948
+
949
+ // Start with a few buckets so small projects still get
950
+ // the benefit of multiple upload streams
951
+ const buckets: {
952
+ files: FileContainer[];
953
+ remainingSize: number;
954
+ }[] = new Array(BULK_UPLOAD_CONCURRENCY).fill(null).map(() => ({
955
+ files: [],
956
+ remainingSize: MAX_BUCKET_SIZE,
957
+ }));
958
+
959
+ let bucketOffset = 0;
960
+ for (const file of sortedFiles) {
961
+ let inserted = false;
962
+
963
+ for (let i = 0; i < buckets.length; i++) {
964
+ // Start at a different bucket for each new file
965
+ const bucket = buckets[(i + bucketOffset) % buckets.length];
966
+ if (
967
+ bucket.remainingSize >= file.sizeInBytes &&
968
+ bucket.files.length < MAX_BUCKET_FILE_COUNT
969
+ ) {
970
+ bucket.files.push(file);
971
+ bucket.remainingSize -= file.sizeInBytes;
972
+ inserted = true;
973
+ break;
974
+ }
975
+ }
976
+
977
+ if (!inserted) {
978
+ buckets.push({
979
+ files: [file],
980
+ remainingSize: MAX_BUCKET_SIZE - file.sizeInBytes,
981
+ });
982
+ }
983
+ bucketOffset++;
984
+ }
985
+
986
+ let counter = fileMap.size - sortedFiles.length;
987
+ const { rerender, unmount } = render(
988
+ <Progress done={counter} total={fileMap.size} />
989
+ );
990
+
991
+ const queue = new PQueue({ concurrency: BULK_UPLOAD_CONCURRENCY });
992
+
993
+ for (const bucket of buckets) {
994
+ // Don't upload empty buckets (can happen for tiny projects)
995
+ if (bucket.files.length === 0) continue;
996
+
997
+ const payload: UploadPayloadFile[] = bucket.files.map((file) => ({
998
+ key: file.hash,
999
+ value: file.content,
1000
+ metadata: {
1001
+ contentType: file.contentType,
1002
+ },
1003
+ base64: true,
1004
+ }));
1005
+
1006
+ let attempts = 0;
1007
+ const doUpload = async (): Promise<void> => {
1008
+ try {
1009
+ return await fetchResult(`/pages/assets/upload`, {
1010
+ method: "POST",
1011
+ headers: {
1012
+ "Content-Type": "application/json",
1013
+ Authorization: `Bearer ${jwt}`,
1014
+ },
1015
+ body: JSON.stringify(payload),
1016
+ });
1017
+ } catch (e) {
1018
+ if (attempts < MAX_UPLOAD_ATTEMPTS) {
1019
+ // Linear backoff, 0 second first time, then 1 second etc.
1020
+ await new Promise((resolve) =>
1021
+ setTimeout(resolve, attempts++ * 1000)
1022
+ );
1023
+
1024
+ if ((e as { code: number }).code === 8000013) {
1025
+ // Looks like the JWT expired, fetch another one
1026
+ jwt = await fetchJwt();
1027
+ }
1028
+ return doUpload();
1029
+ } else {
1030
+ throw e;
1031
+ }
1032
+ }
1033
+ };
1034
+
1035
+ queue.add(() =>
1036
+ doUpload().then(
1037
+ () => {
1038
+ counter += bucket.files.length;
1039
+ rerender(<Progress done={counter} total={fileMap.size} />);
1040
+ },
1041
+ (error) => {
1042
+ return Promise.reject(
1043
+ new FatalError(
1044
+ "Failed to upload files. Please try again.",
1045
+ error.code || 1
1046
+ )
1047
+ );
1048
+ }
1049
+ )
1050
+ );
1051
+ }
1052
+
1053
+ await queue.onIdle();
1054
+
1055
+ unmount();
1056
+
1057
+ const uploadMs = Date.now() - start;
1058
+
1059
+ const skipped = fileMap.size - missingHashes.length;
1060
+ const skippedMessage = skipped > 0 ? `(${skipped} already uploaded) ` : "";
1061
+
1062
+ logger.log(
1063
+ `✨ Success! Uploaded ${
1064
+ sortedFiles.length
1065
+ } files ${skippedMessage}${formatTime(uploadMs)}\n`
1066
+ );
1067
+
1068
+ const doUpsertHashes = async (): Promise<void> => {
1069
+ try {
1070
+ return await fetchResult(`/pages/assets/upsert-hashes`, {
1071
+ method: "POST",
1072
+ headers: {
1073
+ "Content-Type": "application/json",
1074
+ Authorization: `Bearer ${jwt}`,
1075
+ },
1076
+ body: JSON.stringify({
1077
+ hashes: files.map(({ hash }) => hash),
1078
+ }),
1079
+ });
1080
+ } catch (e) {
1081
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1082
+
1083
+ if ((e as { code: number }).code === 8000013) {
1084
+ // Looks like the JWT expired, fetch another one
1085
+ jwt = await fetchJwt();
1086
+ }
1087
+
1088
+ return await fetchResult(`/pages/assets/upsert-hashes`, {
1089
+ method: "POST",
1090
+ headers: {
1091
+ "Content-Type": "application/json",
1092
+ Authorization: `Bearer ${jwt}`,
1093
+ },
1094
+ body: JSON.stringify({
1095
+ hashes: files.map(({ hash }) => hash),
1096
+ }),
1097
+ });
1098
+ }
1099
+ };
1100
+
1101
+ try {
1102
+ await doUpsertHashes();
1103
+ } catch {
1104
+ logger.warn(
1105
+ "Failed to update file hashes. Every upload appeared to succeed for this deployment, but you might need to re-upload for future deployments. This shouldn't have any impact other than slowing the upload speed of your next deployment."
1106
+ );
1107
+ }
1108
+
1109
+ return Object.fromEntries(
1110
+ [...fileMap.entries()].map(([fileName, file]) => [
1111
+ `/${fileName}`,
1112
+ file.hash,
1113
+ ])
1114
+ );
1115
+ };
1116
+
1117
+ const createDeployment: CommandModule<
1118
+ CreateDeploymentArgs,
1119
+ CreateDeploymentArgs
1120
+ > = {
1121
+ describe: "🆙 Publish a directory of static assets as a Pages deployment",
1122
+ builder: (yargs) => {
1123
+ return yargs
1124
+ .positional("directory", {
1125
+ type: "string",
1126
+ demandOption: true,
1127
+ description: "The directory of static files to upload",
1128
+ })
1129
+ .options({
1130
+ "project-name": {
1131
+ type: "string",
1132
+ description: "The name of the project you want to deploy to",
1133
+ },
1134
+ branch: {
1135
+ type: "string",
1136
+ description: "The name of the branch you want to deploy to",
1137
+ },
1138
+ "commit-hash": {
1139
+ type: "string",
1140
+ description: "The SHA to attach to this deployment",
1141
+ },
1142
+ "commit-message": {
1143
+ type: "string",
1144
+ description: "The commit message to attach to this deployment",
1145
+ },
1146
+ "commit-dirty": {
1147
+ type: "boolean",
1148
+ description:
1149
+ "Whether or not the workspace should be considered dirty for this deployment",
1150
+ },
1151
+ })
1152
+ .epilogue(pagesBetaWarning);
1153
+ },
1154
+ handler: async ({
1155
+ directory,
1156
+ projectName,
1157
+ branch,
1158
+ commitHash,
1159
+ commitMessage,
1160
+ commitDirty,
1161
+ }) => {
1162
+ if (!directory) {
1163
+ throw new FatalError("Must specify a directory.", 1);
1164
+ }
1165
+
1166
+ const config = getConfigCache<PagesConfigCache>(
1167
+ PAGES_CONFIG_CACHE_FILENAME
1168
+ );
1169
+ const accountId = await requireAuth(config);
1170
+
1171
+ projectName ??= config.project_name;
1172
+
1173
+ const isInteractive = process.stdin.isTTY;
1174
+ if (!projectName && isInteractive) {
1175
+ const projects = (await listProjects({ accountId })).filter(
1176
+ (project) => !project.source
1177
+ );
1178
+
1179
+ let existingOrNew: "existing" | "new" = "new";
1180
+
1181
+ if (projects.length > 0) {
1182
+ existingOrNew = await new Promise<"new" | "existing">((resolve) => {
1183
+ const { unmount } = render(
1184
+ <>
1185
+ <Text>
1186
+ No project selected. Would you like to create one or use an
1187
+ existing project?
1188
+ </Text>
1189
+ <SelectInput
1190
+ items={[
1191
+ {
1192
+ key: "new",
1193
+ label: "Create a new project",
1194
+ value: "new",
1195
+ },
1196
+ {
1197
+ key: "existing",
1198
+ label: "Use an existing project",
1199
+ value: "existing",
1200
+ },
1201
+ ]}
1202
+ onSelect={async (selected) => {
1203
+ resolve(selected.value as "new" | "existing");
1204
+ unmount();
1205
+ }}
1206
+ />
1207
+ </>
1208
+ );
1209
+ });
1210
+ }
1211
+
1212
+ switch (existingOrNew) {
1213
+ case "existing": {
1214
+ projectName = await new Promise((resolve) => {
1215
+ const { unmount } = render(
1216
+ <>
1217
+ <Text>Select a project:</Text>
1218
+ <SelectInput
1219
+ items={projects.map((project) => ({
1220
+ key: project.name,
1221
+ label: project.name,
1222
+ value: project,
1223
+ }))}
1224
+ onSelect={async (selected) => {
1225
+ resolve(selected.value.name);
1226
+ unmount();
1227
+ }}
1228
+ />
1229
+ </>
1230
+ );
1231
+ });
1232
+ break;
1233
+ }
1234
+ case "new": {
1235
+ projectName = await prompt("Enter the name of your new project:");
1236
+
1237
+ if (!projectName) {
1238
+ throw new FatalError("Must specify a project name.", 1);
1239
+ }
1240
+
1241
+ let isGitDir = true;
1242
+ try {
1243
+ execSync(`git rev-parse --is-inside-work-tree`, {
1244
+ stdio: "ignore",
1245
+ });
1246
+ } catch (err) {
1247
+ isGitDir = false;
1248
+ }
1249
+
1250
+ const productionBranch = await prompt(
1251
+ "Enter the production branch name:",
1252
+ "text",
1253
+ isGitDir
1254
+ ? execSync(`git rev-parse --abbrev-ref HEAD`).toString().trim()
1255
+ : "production"
1256
+ );
1257
+
1258
+ if (!productionBranch) {
1259
+ throw new FatalError("Must specify a production branch.", 1);
1260
+ }
1261
+
1262
+ await fetchResult<Project>(`/accounts/${accountId}/pages/projects`, {
1263
+ method: "POST",
1264
+ body: JSON.stringify({
1265
+ name: projectName,
1266
+ production_branch: productionBranch,
1267
+ }),
1268
+ });
1269
+
1270
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1271
+ account_id: accountId,
1272
+ project_name: projectName,
1273
+ });
1274
+
1275
+ logger.log(`✨ Successfully created the '${projectName}' project.`);
1276
+ break;
1277
+ }
1278
+ }
1279
+ }
1280
+
1281
+ if (!projectName) {
1282
+ throw new FatalError("Must specify a project name.", 1);
1283
+ }
1284
+
1285
+ // We infer git info by default is not passed in
1286
+
1287
+ let isGitDir = true;
1288
+ try {
1289
+ execSync(`git rev-parse --is-inside-work-tree`, {
1290
+ stdio: "ignore",
1291
+ });
1292
+ } catch (err) {
1293
+ isGitDir = false;
1294
+ }
1295
+
1296
+ let isGitDirty = false;
1297
+
1298
+ if (isGitDir) {
1299
+ try {
1300
+ isGitDirty = Boolean(
1301
+ execSync(`git status --porcelain`).toString().length
1302
+ );
1303
+
1304
+ if (!branch) {
1305
+ branch = execSync(`git rev-parse --abbrev-ref HEAD`)
1306
+ .toString()
1307
+ .trim();
1308
+ }
1309
+
1310
+ if (!commitHash) {
1311
+ commitHash = execSync(`git rev-parse HEAD`).toString().trim();
1312
+ }
1313
+
1314
+ if (!commitMessage) {
1315
+ commitMessage = execSync(`git show -s --format=%B ${commitHash}`)
1316
+ .toString()
1317
+ .trim();
1318
+ }
1319
+ } catch (err) {}
1320
+
1321
+ if (isGitDirty && !commitDirty) {
1322
+ logger.warn(
1323
+ `Warning: Your working directory is a git repo and has uncommitted changes\nTo silence this warning, pass in --commit-dirty=true`
1324
+ );
1325
+ }
1326
+
1327
+ if (commitDirty === undefined) {
1328
+ commitDirty = isGitDirty;
1329
+ }
1330
+ }
1331
+
1332
+ let builtFunctions: string | undefined = undefined;
1333
+ const functionsDirectory = join(cwd(), "functions");
1334
+ if (existsSync(functionsDirectory)) {
1335
+ const outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`);
1336
+
1337
+ await new Promise((resolve) =>
1338
+ buildFunctions({
1339
+ outfile,
1340
+ functionsDirectory,
1341
+ onEnd: () => resolve(null),
1342
+ buildOutputDirectory: dirname(outfile),
1343
+ })
1344
+ );
1345
+
1346
+ builtFunctions = readFileSync(outfile, "utf-8");
1347
+ }
1348
+
1349
+ const manifest = await upload({ directory, accountId, projectName });
1350
+
1351
+ const formData = new FormData();
1352
+
1353
+ formData.append("manifest", JSON.stringify(manifest));
1354
+
1355
+ if (branch) {
1356
+ formData.append("branch", branch);
1357
+ }
1358
+
1359
+ if (commitMessage) {
1360
+ formData.append("commit_message", commitMessage);
1361
+ }
1362
+
1363
+ if (commitHash) {
1364
+ formData.append("commit_hash", commitHash);
1365
+ }
1366
+
1367
+ if (commitDirty !== undefined) {
1368
+ formData.append("commit_dirty", commitDirty);
1369
+ }
1370
+
1371
+ let _headers: string | undefined,
1372
+ _redirects: string | undefined,
1373
+ _workerJS: string | undefined;
1374
+
1375
+ try {
1376
+ _headers = readFileSync(join(directory, "_headers"), "utf-8");
1377
+ } catch {}
1378
+
1379
+ try {
1380
+ _redirects = readFileSync(join(directory, "_redirects"), "utf-8");
1381
+ } catch {}
1382
+
1383
+ try {
1384
+ _workerJS = readFileSync(join(directory, "_worker.js"), "utf-8");
1385
+ } catch {}
1386
+
1387
+ if (_headers) {
1388
+ formData.append("_headers", new File([_headers], "_headers"));
1389
+ }
1390
+
1391
+ if (_redirects) {
1392
+ formData.append("_redirects", new File([_redirects], "_redirects"));
1393
+ }
1394
+
1395
+ if (builtFunctions) {
1396
+ formData.append("_worker.js", new File([builtFunctions], "_worker.js"));
1397
+ } else if (_workerJS) {
1398
+ formData.append("_worker.js", new File([_workerJS], "_worker.js"));
1399
+ }
1400
+
1401
+ const deploymentResponse = await fetchResult<Deployment>(
1402
+ `/accounts/${accountId}/pages/projects/${projectName}/deployments`,
1403
+ {
1404
+ method: "POST",
1405
+ body: formData,
1406
+ }
1407
+ );
1408
+
1409
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1410
+ account_id: accountId,
1411
+ project_name: projectName,
1412
+ });
1413
+
1414
+ logger.log(
1415
+ `✨ Deployment complete! Take a peek over at ${deploymentResponse.url}`
1416
+ );
1417
+ },
1418
+ };
1419
+
700
1420
  export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
701
1421
  return yargs
702
1422
  .command(
703
- "dev [directory] [-- command]",
1423
+ "dev [directory] [-- command..]",
704
1424
  "🧑‍💻 Develop your full-stack Pages application locally",
705
1425
  (yargs) => {
706
1426
  return yargs
@@ -756,8 +1476,15 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
756
1476
  default: false,
757
1477
  description: "Auto reload HTML pages when change is detected",
758
1478
  },
1479
+ "node-compat": {
1480
+ describe: "Enable node.js compatibility",
1481
+ default: false,
1482
+ type: "boolean",
1483
+ hidden: true,
1484
+ },
759
1485
  // TODO: Miniflare user options
760
- });
1486
+ })
1487
+ .epilogue(pagesBetaWarning);
761
1488
  },
762
1489
  async ({
763
1490
  local,
@@ -769,10 +1496,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
769
1496
  kv: kvs = [],
770
1497
  do: durableObjects = [],
771
1498
  "live-reload": liveReload,
772
- "--": remaining = [],
1499
+ "node-compat": nodeCompat,
1500
+ _: [_pages, _dev, ...remaining],
773
1501
  }) => {
1502
+ // Beta message for `wrangler pages <commands>` usage
1503
+ logger.log(pagesBetaWarning);
1504
+
774
1505
  if (!local) {
775
- console.error("Only local mode is supported at the moment.");
1506
+ logger.error("Only local mode is supported at the moment.");
776
1507
  return;
777
1508
  }
778
1509
 
@@ -793,41 +1524,59 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
793
1524
 
794
1525
  let miniflareArgs: MiniflareOptions = {};
795
1526
 
796
- let scriptReadyResolve;
797
- const scriptReadyPromise = new Promise(
1527
+ let scriptReadyResolve: () => void;
1528
+ const scriptReadyPromise = new Promise<void>(
798
1529
  (resolve) => (scriptReadyResolve = resolve)
799
1530
  );
800
1531
 
801
1532
  if (usingFunctions) {
802
- const scriptPath = join(tmpdir(), "./functionsWorker.js");
1533
+ const outfile = join(
1534
+ tmpdir(),
1535
+ `./functionsWorker-${Math.random()}.js`
1536
+ );
803
1537
 
804
- console.log(`Compiling worker to "${scriptPath}"...`);
1538
+ if (nodeCompat) {
1539
+ console.warn(
1540
+ "Enabling node.js compatibility mode for builtins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
1541
+ );
1542
+ }
805
1543
 
806
- await buildFunctions({
807
- scriptPath,
808
- functionsDirectory,
809
- sourcemap: true,
810
- watch: true,
811
- onEnd: () => scriptReadyResolve(),
812
- });
1544
+ logger.log(`Compiling worker to "${outfile}"...`);
1545
+
1546
+ try {
1547
+ await buildFunctions({
1548
+ outfile,
1549
+ functionsDirectory,
1550
+ sourcemap: true,
1551
+ watch: true,
1552
+ onEnd: () => scriptReadyResolve(),
1553
+ buildOutputDirectory: directory,
1554
+ nodeCompat,
1555
+ });
1556
+ } catch {}
813
1557
 
814
1558
  watch([functionsDirectory], {
815
1559
  persistent: true,
816
1560
  ignoreInitial: true,
817
1561
  }).on("all", async () => {
818
1562
  await buildFunctions({
819
- scriptPath,
1563
+ outfile,
820
1564
  functionsDirectory,
821
1565
  sourcemap: true,
822
1566
  watch: true,
823
1567
  onEnd: () => scriptReadyResolve(),
1568
+ buildOutputDirectory: directory,
1569
+ nodeCompat,
824
1570
  });
825
1571
  });
826
1572
 
827
1573
  miniflareArgs = {
828
- scriptPath,
1574
+ scriptPath: outfile,
829
1575
  };
830
1576
  } else {
1577
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1578
+ scriptReadyResolve!();
1579
+
831
1580
  const scriptPath =
832
1581
  directory !== undefined
833
1582
  ? join(directory, singleWorkerScriptPath)
@@ -838,8 +1587,11 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
838
1587
  scriptPath,
839
1588
  };
840
1589
  } else {
841
- console.log("No functions. Shimming...");
1590
+ logger.log("No functions. Shimming...");
842
1591
  miniflareArgs = {
1592
+ // cfFetch sets the `cf` object that a function could expect
1593
+ // If there are no functions, there's no reason to set this up (and not make that network call)
1594
+ cfFetch: false,
843
1595
  // TODO: The fact that these request/response hacks are necessary is ridiculous.
844
1596
  // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
845
1597
  script: `
@@ -850,7 +1602,6 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
850
1602
  }
851
1603
  }`,
852
1604
  };
853
- scriptReadyResolve();
854
1605
  }
855
1606
  }
856
1607
 
@@ -871,6 +1622,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
871
1622
  ? await generateAssetsFetch(directory)
872
1623
  : invalidAssetsFetch;
873
1624
 
1625
+ const requestContextCheckOptions =
1626
+ await getRequestContextCheckOptions();
1627
+
874
1628
  const miniflare = new Miniflare({
875
1629
  port,
876
1630
  watch: true,
@@ -891,20 +1645,22 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
891
1645
  // User bindings
892
1646
  bindings: {
893
1647
  ...Object.fromEntries(
894
- bindings.map((binding) => binding.toString().split("="))
1648
+ bindings
1649
+ .map((binding) => binding.toString().split("="))
1650
+ .map(([key, ...values]) => [key, values.join("=")])
895
1651
  ),
896
1652
  },
897
1653
 
898
1654
  // env.ASSETS.fetch
899
1655
  serviceBindings: {
900
- async ASSETS(request) {
1656
+ async ASSETS(request: MiniflareRequest) {
901
1657
  if (proxyPort) {
902
1658
  try {
903
1659
  const url = new URL(request.url);
904
1660
  url.host = `localhost:${proxyPort}`;
905
1661
  return await fetch(url, request);
906
1662
  } catch (thrown) {
907
- console.error(`Could not proxy request: ${thrown}`);
1663
+ logger.error(`Could not proxy request: ${thrown}`);
908
1664
 
909
1665
  // TODO: Pretty error page
910
1666
  return new Response(
@@ -916,7 +1672,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
916
1672
  try {
917
1673
  return await assetsFetch(request);
918
1674
  } catch (thrown) {
919
- console.error(`Could not serve static asset: ${thrown}`);
1675
+ logger.error(`Could not serve static asset: ${thrown}`);
920
1676
 
921
1677
  // TODO: Pretty error page
922
1678
  return new Response(
@@ -933,18 +1689,17 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
933
1689
  cachePersist: true,
934
1690
  liveReload,
935
1691
 
1692
+ ...requestContextCheckOptions,
936
1693
  ...miniflareArgs,
937
1694
  });
938
1695
 
939
1696
  try {
940
1697
  // `startServer` might throw if user code contains errors
941
1698
  const server = await miniflare.startServer();
942
- console.log(`Serving at http://localhost:${port}/`);
1699
+ logger.log(`Serving at http://localhost:${port}/`);
943
1700
 
944
1701
  if (process.env.BROWSER !== "none") {
945
- const childProcess = await open(`http://localhost:${port}/`);
946
- // fail silently if the open command doesn't work (e.g. in GitHub Codespaces)
947
- childProcess.on("error", (_err) => {});
1702
+ await openInBrowser(`http://localhost:${port}/`);
948
1703
  }
949
1704
 
950
1705
  if (directory !== undefined && liveReload) {
@@ -956,13 +1711,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
956
1711
  });
957
1712
  }
958
1713
 
959
- EXIT_CALLBACKS.push(() => {
1714
+ CLEANUP_CALLBACKS.push(() => {
960
1715
  server.close();
961
1716
  miniflare.dispose().catch((err) => miniflare.log.error(err));
962
1717
  });
963
1718
  } catch (e) {
964
- miniflare.log.error(e);
965
- EXIT("Could not start Miniflare.", 1);
1719
+ miniflare.log.error(e as Error);
1720
+ CLEANUP();
1721
+ throw new FatalError("Could not start Miniflare.", 1);
966
1722
  }
967
1723
  }
968
1724
  )
@@ -980,7 +1736,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
980
1736
  description: "The directory of Pages Functions",
981
1737
  })
982
1738
  .options({
983
- "script-path": {
1739
+ outfile: {
984
1740
  type: "string",
985
1741
  default: "_worker.js",
986
1742
  description: "The location of the output Worker script",
@@ -1012,32 +1768,326 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1012
1768
  description:
1013
1769
  "Watch for changes to the functions and automatically rebuild the Worker script",
1014
1770
  },
1015
- }),
1771
+ plugin: {
1772
+ type: "boolean",
1773
+ default: false,
1774
+ description: "Build a plugin rather than a Worker script",
1775
+ },
1776
+ "build-output-directory": {
1777
+ type: "string",
1778
+ description: "The directory to output static assets to",
1779
+ },
1780
+ "node-compat": {
1781
+ describe: "Enable node.js compatibility",
1782
+ default: false,
1783
+ type: "boolean",
1784
+ hidden: true,
1785
+ },
1786
+ })
1787
+ .epilogue(pagesBetaWarning),
1016
1788
  async ({
1017
1789
  directory,
1018
- "script-path": scriptPath,
1790
+ outfile,
1019
1791
  "output-config-path": outputConfigPath,
1020
1792
  minify,
1021
1793
  sourcemap,
1022
1794
  fallbackService,
1023
1795
  watch,
1796
+ plugin,
1797
+ "build-output-directory": buildOutputDirectory,
1798
+ "node-compat": nodeCompat,
1024
1799
  }) => {
1800
+ if (!isInPagesCI) {
1801
+ // Beta message for `wrangler pages <commands>` usage
1802
+ logger.log(pagesBetaWarning);
1803
+ }
1804
+
1805
+ if (nodeCompat) {
1806
+ console.warn(
1807
+ "Enabling node.js compatibility mode for builtins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
1808
+ );
1809
+ }
1810
+
1811
+ buildOutputDirectory ??= dirname(outfile);
1812
+
1025
1813
  await buildFunctions({
1026
- scriptPath,
1814
+ outfile,
1027
1815
  outputConfigPath,
1028
1816
  functionsDirectory: directory,
1029
1817
  minify,
1030
1818
  sourcemap,
1031
1819
  fallbackService,
1032
1820
  watch,
1821
+ plugin,
1822
+ buildOutputDirectory,
1823
+ nodeCompat,
1033
1824
  });
1034
1825
  }
1035
1826
  )
1036
- );
1827
+ )
1828
+ .command("project", "⚡️ Interact with your Pages projects", (yargs) =>
1829
+ yargs
1830
+ .command(
1831
+ "list",
1832
+ "List your Cloudflare Pages projects",
1833
+ (yargs) => yargs.epilogue(pagesBetaWarning),
1834
+ async () => {
1835
+ const config = getConfigCache<PagesConfigCache>(
1836
+ PAGES_CONFIG_CACHE_FILENAME
1837
+ );
1838
+
1839
+ const accountId = await requireAuth(config);
1840
+
1841
+ const projects: Array<Project> = await listProjects({ accountId });
1842
+
1843
+ const data = projects.map((project) => {
1844
+ return {
1845
+ "Project Name": project.name,
1846
+ "Project Domains": `${project.domains.join(", ")}`,
1847
+ "Git Provider": project.source ? "Yes" : "No",
1848
+ "Last Modified": project.latest_deployment
1849
+ ? timeagoFormat(project.latest_deployment.modified_on)
1850
+ : timeagoFormat(project.created_on),
1851
+ };
1852
+ });
1853
+
1854
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1855
+ account_id: accountId,
1856
+ });
1857
+
1858
+ render(<Table data={data}></Table>);
1859
+ }
1860
+ )
1861
+ .command(
1862
+ "create [project-name]",
1863
+ "Create a new Cloudflare Pages project",
1864
+ (yargs) =>
1865
+ yargs
1866
+ .positional("project-name", {
1867
+ type: "string",
1868
+ demandOption: true,
1869
+ description: "The name of your Pages project",
1870
+ })
1871
+ .options({
1872
+ "production-branch": {
1873
+ type: "string",
1874
+ description:
1875
+ "The name of the production branch of your project",
1876
+ },
1877
+ })
1878
+ .epilogue(pagesBetaWarning),
1879
+ async ({ productionBranch, projectName }) => {
1880
+ const config = getConfigCache<PagesConfigCache>(
1881
+ PAGES_CONFIG_CACHE_FILENAME
1882
+ );
1883
+ const accountId = await requireAuth(config);
1884
+
1885
+ const isInteractive = process.stdin.isTTY;
1886
+ if (!projectName && isInteractive) {
1887
+ projectName = await prompt("Enter the name of your new project:");
1888
+ }
1889
+
1890
+ if (!projectName) {
1891
+ throw new FatalError("Must specify a project name.", 1);
1892
+ }
1893
+
1894
+ if (!productionBranch && isInteractive) {
1895
+ let isGitDir = true;
1896
+ try {
1897
+ execSync(`git rev-parse --is-inside-work-tree`, {
1898
+ stdio: "ignore",
1899
+ });
1900
+ } catch (err) {
1901
+ isGitDir = false;
1902
+ }
1903
+
1904
+ productionBranch = await prompt(
1905
+ "Enter the production branch name:",
1906
+ "text",
1907
+ isGitDir
1908
+ ? execSync(`git rev-parse --abbrev-ref HEAD`)
1909
+ .toString()
1910
+ .trim()
1911
+ : "production"
1912
+ );
1913
+ }
1914
+
1915
+ if (!productionBranch) {
1916
+ throw new FatalError("Must specify a production branch.", 1);
1917
+ }
1918
+
1919
+ const { subdomain } = await fetchResult<Project>(
1920
+ `/accounts/${accountId}/pages/projects`,
1921
+ {
1922
+ method: "POST",
1923
+ body: JSON.stringify({
1924
+ name: projectName,
1925
+ production_branch: productionBranch,
1926
+ }),
1927
+ }
1928
+ );
1929
+
1930
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1931
+ account_id: accountId,
1932
+ project_name: projectName,
1933
+ });
1934
+
1935
+ logger.log(
1936
+ `✨ Successfully created the '${projectName}' project. It will be available at https://${subdomain}/ once you create your first deployment.`
1937
+ );
1938
+ logger.log(
1939
+ `To deploy a folder of assets, run 'wrangler pages publish [directory]'.`
1940
+ );
1941
+ }
1942
+ )
1943
+ .epilogue(pagesBetaWarning)
1944
+ )
1945
+ .command(
1946
+ "deployment",
1947
+ "🚀 Interact with the deployments of a project",
1948
+ (yargs) =>
1949
+ yargs
1950
+ .command(
1951
+ "list",
1952
+ "List deployments in your Cloudflare Pages project",
1953
+ (yargs) =>
1954
+ yargs
1955
+ .options({
1956
+ "project-name": {
1957
+ type: "string",
1958
+ description:
1959
+ "The name of the project you would like to list deployments for",
1960
+ },
1961
+ })
1962
+ .epilogue(pagesBetaWarning),
1963
+ async ({ projectName }) => {
1964
+ const config = getConfigCache<PagesConfigCache>(
1965
+ PAGES_CONFIG_CACHE_FILENAME
1966
+ );
1967
+ const accountId = await requireAuth(config);
1968
+
1969
+ projectName ??= config.project_name;
1970
+
1971
+ const isInteractive = process.stdin.isTTY;
1972
+ if (!projectName && isInteractive) {
1973
+ const projects = await listProjects({ accountId });
1974
+ projectName = await new Promise((resolve) => {
1975
+ const { unmount } = render(
1976
+ <>
1977
+ <Text>Select a project:</Text>
1978
+ <SelectInput
1979
+ items={projects.map((project) => ({
1980
+ key: project.name,
1981
+ label: project.name,
1982
+ value: project,
1983
+ }))}
1984
+ onSelect={async (selected) => {
1985
+ resolve(selected.value.name);
1986
+ unmount();
1987
+ }}
1988
+ />
1989
+ </>
1990
+ );
1991
+ });
1992
+ }
1993
+
1994
+ if (!projectName) {
1995
+ throw new FatalError("Must specify a project name.", 1);
1996
+ }
1997
+
1998
+ const deployments: Array<Deployment> = await fetchResult(
1999
+ `/accounts/${accountId}/pages/projects/${projectName}/deployments`
2000
+ );
2001
+
2002
+ const titleCase = (word: string) =>
2003
+ word.charAt(0).toUpperCase() + word.slice(1);
2004
+
2005
+ const shortSha = (sha: string) => sha.slice(0, 7);
2006
+
2007
+ const getStatus = (deployment: Deployment) => {
2008
+ // Return a pretty time since timestamp if successful otherwise the status
2009
+ if (deployment.latest_stage.status === `success`) {
2010
+ return timeagoFormat(deployment.latest_stage.ended_on);
2011
+ }
2012
+ return titleCase(deployment.latest_stage.status);
2013
+ };
2014
+
2015
+ const data = deployments.map((deployment) => {
2016
+ return {
2017
+ Environment: titleCase(deployment.environment),
2018
+ Branch: deployment.deployment_trigger.metadata.branch,
2019
+ Source: shortSha(
2020
+ deployment.deployment_trigger.metadata.commit_hash
2021
+ ),
2022
+ Deployment: deployment.url,
2023
+ Status: getStatus(deployment),
2024
+ // TODO: Use a url shortener
2025
+ Build: `https://dash.cloudflare.com/${accountId}/pages/view/${deployment.project_name}/${deployment.id}`,
2026
+ };
2027
+ });
2028
+
2029
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
2030
+ account_id: accountId,
2031
+ });
2032
+
2033
+ render(<Table data={data}></Table>);
2034
+ }
2035
+ )
2036
+ .command({
2037
+ command: "create [directory]",
2038
+ ...createDeployment,
2039
+ } as CommandModule)
2040
+ .epilogue(pagesBetaWarning)
2041
+ )
2042
+ .command({
2043
+ command: "publish [directory]",
2044
+ ...createDeployment,
2045
+ } as CommandModule);
1037
2046
  };
1038
2047
 
1039
- const invalidAssetsFetch: typeof fetch = () => {
2048
+ const invalidAssetsFetch: typeof miniflareFetch = () => {
1040
2049
  throw new Error(
1041
2050
  "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
1042
2051
  );
1043
2052
  };
2053
+
2054
+ const listProjects = async ({
2055
+ accountId,
2056
+ }: {
2057
+ accountId: string;
2058
+ }): Promise<Array<Project>> => {
2059
+ const pageSize = 10;
2060
+ let page = 1;
2061
+ const results = [];
2062
+ while (results.length % pageSize === 0) {
2063
+ const json: Array<Project> = await fetchResult(
2064
+ `/accounts/${accountId}/pages/projects`,
2065
+ {},
2066
+ new URLSearchParams({
2067
+ per_page: pageSize.toString(),
2068
+ page: page.toString(),
2069
+ })
2070
+ );
2071
+ page++;
2072
+ results.push(...json);
2073
+ if (json.length < pageSize) {
2074
+ break;
2075
+ }
2076
+ }
2077
+ return results;
2078
+ };
2079
+
2080
+ function formatTime(duration: number) {
2081
+ return `(${(duration / 1000).toFixed(2)} sec)`;
2082
+ }
2083
+
2084
+ function Progress({ done, total }: { done: number; total: number }) {
2085
+ return (
2086
+ <>
2087
+ <Text>
2088
+ <Spinner type="earth" />
2089
+ {` Uploading... (${done}/${total})\n`}
2090
+ </Text>
2091
+ </>
2092
+ );
2093
+ }