wrangler 0.0.0-e928f94 → 0.0.0-e978986

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 (124) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +48 -4
  4. package/miniflare-dist/index.mjs +4886 -0
  5. package/package.json +74 -34
  6. package/pages/functions/buildPlugin.ts +105 -0
  7. package/pages/functions/buildWorker.ts +151 -0
  8. package/pages/functions/filepath-routing.test.ts +234 -0
  9. package/pages/functions/filepath-routing.ts +189 -0
  10. package/pages/functions/identifiers.ts +78 -0
  11. package/pages/functions/routes.ts +156 -0
  12. package/pages/functions/template-plugin.ts +147 -0
  13. package/pages/functions/template-worker.ts +143 -0
  14. package/src/__tests__/config-cache.test.ts +36 -0
  15. package/src/__tests__/configuration.test.ts +3374 -0
  16. package/src/__tests__/dev.test.tsx +980 -0
  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__/helpers/mock-cfetch.ts +211 -0
  24. package/src/__tests__/helpers/mock-console.ts +87 -0
  25. package/src/__tests__/helpers/mock-dialogs.ts +130 -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__/helpers/mock-kv.ts +33 -0
  29. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  30. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  31. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  32. package/src/__tests__/helpers/run-in-tmp.ts +39 -0
  33. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  34. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  35. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  36. package/src/__tests__/https-options.test.ts +130 -0
  37. package/src/__tests__/index.test.ts +233 -130
  38. package/src/__tests__/init.test.ts +1567 -0
  39. package/src/__tests__/jest.setup.ts +77 -0
  40. package/src/__tests__/kv.test.ts +1481 -0
  41. package/src/__tests__/logger.test.ts +115 -0
  42. package/src/__tests__/package-manager.test.ts +353 -0
  43. package/src/__tests__/pages.test.ts +657 -0
  44. package/src/__tests__/parse.test.ts +330 -0
  45. package/src/__tests__/publish.test.ts +5468 -0
  46. package/src/__tests__/r2.test.ts +233 -0
  47. package/src/__tests__/route.test.ts +45 -0
  48. package/src/__tests__/secret.test.ts +481 -0
  49. package/src/__tests__/tail.test.ts +690 -0
  50. package/src/__tests__/user.test.ts +131 -0
  51. package/src/__tests__/whoami.test.tsx +105 -0
  52. package/src/abort.d.ts +3 -0
  53. package/src/bundle.ts +200 -0
  54. package/src/cfetch/index.ts +121 -0
  55. package/src/cfetch/internal.ts +145 -0
  56. package/src/cli.ts +10 -7
  57. package/src/config/README.md +107 -0
  58. package/src/config/config.ts +234 -0
  59. package/src/config/diagnostics.ts +80 -0
  60. package/src/config/environment.ts +420 -0
  61. package/src/config/index.ts +225 -0
  62. package/src/config/validation-helpers.ts +575 -0
  63. package/src/config/validation.ts +1539 -0
  64. package/src/config-cache.ts +45 -0
  65. package/src/create-worker-preview.ts +185 -0
  66. package/src/create-worker-upload-form.ts +254 -0
  67. package/src/dev/dev-vars.ts +36 -0
  68. package/src/dev/dev.tsx +419 -0
  69. package/src/dev/local.tsx +318 -0
  70. package/src/dev/remote.tsx +227 -0
  71. package/src/dev/use-esbuild.ts +118 -0
  72. package/src/dialogs.tsx +67 -17
  73. package/src/durable.ts +102 -0
  74. package/src/entry.ts +322 -0
  75. package/src/environment-variables.ts +35 -0
  76. package/src/errors.ts +11 -0
  77. package/src/https-options.ts +123 -0
  78. package/src/index.tsx +2031 -1041
  79. package/src/inspect.ts +662 -0
  80. package/src/intl-polyfill.d.ts +139 -0
  81. package/src/kv.ts +408 -0
  82. package/src/logger.ts +66 -0
  83. package/src/miniflare-cli/README.md +30 -0
  84. package/src/miniflare-cli/enum-keys.ts +17 -0
  85. package/src/miniflare-cli/index.ts +41 -0
  86. package/src/miniflare-cli/request-context.ts +40 -0
  87. package/src/module-collection.ts +229 -35
  88. package/src/open-in-browser.ts +19 -0
  89. package/src/package-manager.ts +174 -0
  90. package/src/pages.tsx +1444 -260
  91. package/src/parse.ts +202 -0
  92. package/src/paths.ts +26 -0
  93. package/src/proxy.ts +465 -0
  94. package/src/publish.ts +498 -261
  95. package/src/r2.ts +50 -0
  96. package/src/selfsigned.d.ts +29 -0
  97. package/src/sites.tsx +240 -60
  98. package/src/tail/filters.ts +279 -0
  99. package/src/tail/index.ts +299 -0
  100. package/src/tail/printing.ts +67 -0
  101. package/src/update-check.ts +19 -0
  102. package/src/user.tsx +395 -221
  103. package/src/whoami.tsx +70 -0
  104. package/src/worker.ts +180 -0
  105. package/templates/gitignore +171 -0
  106. package/templates/new-worker-scheduled.js +17 -0
  107. package/templates/new-worker-scheduled.ts +32 -0
  108. package/templates/new-worker.js +15 -0
  109. package/templates/new-worker.ts +30 -0
  110. package/{static-asset-facade.js → templates/static-asset-facade.js} +1 -1
  111. package/templates/tsconfig.json +105 -0
  112. package/wrangler-dist/cli.js +118298 -116056
  113. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  114. package/src/__tests__/mock-cfetch.js +0 -46
  115. package/src/api/form_data.ts +0 -141
  116. package/src/api/inspect.ts +0 -441
  117. package/src/api/preview.ts +0 -128
  118. package/src/api/worker.ts +0 -161
  119. package/src/cfetch.ts +0 -72
  120. package/src/config.ts +0 -122
  121. package/src/dev.tsx +0 -724
  122. package/src/kv.tsx +0 -211
  123. package/src/tail.tsx +0 -71
  124. package/wrangler-dist/cli.js.map +0 -7
package/src/pages.tsx CHANGED
@@ -1,24 +1,131 @@
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 { 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";
10
11
  import { watch } from "chokidar";
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";
23
+ import { buildWorker } from "../pages/functions/buildWorker";
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
+ };
11
75
 
12
- type Exit = (message?: string) => undefined;
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;
93
+
94
+ // Defer importing miniflare until we really need it. This takes ~0.5s
95
+ // and also modifies some `stream/web` and `undici` prototypes, so we
96
+ // don't want to do this if pages commands aren't being called.
97
+
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";
13
100
 
14
- const isWindows = () => process.platform === "win32";
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?.());
107
+ };
108
+
109
+ process.on("SIGINT", () => {
110
+ CLEANUP();
111
+ process.exit();
112
+ });
113
+ process.on("SIGTERM", () => {
114
+ CLEANUP();
115
+ process.exit();
116
+ });
117
+
118
+ function isWindows() {
119
+ return process.platform === "win32";
120
+ }
15
121
 
16
122
  const SECONDS_TO_WAIT_FOR_PROXY = 5;
17
123
 
18
- const sleep = async (ms: number) =>
124
+ async function sleep(ms: number) {
19
125
  await new Promise((resolve) => setTimeout(resolve, ms));
126
+ }
20
127
 
21
- const getPids = (pid: number) => {
128
+ function getPids(pid: number) {
22
129
  const pids: number[] = [pid];
23
130
  let command: string, regExp: RegExp;
24
131
 
@@ -43,9 +150,9 @@ const getPids = (pid: number) => {
43
150
  } catch {}
44
151
 
45
152
  return pids;
46
- };
153
+ }
47
154
 
48
- const getPort = (pid: number) => {
155
+ function getPort(pid: number) {
49
156
  let command: string, regExp: RegExp;
50
157
 
51
158
  if (isWindows()) {
@@ -66,31 +173,28 @@ const getPort = (pid: number) => {
66
173
  const match = matches[0];
67
174
  if (match) return parseInt(match[1]);
68
175
  } catch (thrown) {
69
- console.error(
176
+ logger.error(
70
177
  `Error scanning for ports of process with PID ${pid}: ${thrown}`
71
178
  );
72
179
  }
73
- };
180
+ }
74
181
 
75
- const spawnProxyProcess = async ({
182
+ async function spawnProxyProcess({
76
183
  port,
77
184
  command,
78
185
  }: {
79
186
  port?: number;
80
187
  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
-
88
- if (command.length === 0)
89
- return exit(
90
- "Must specify a directory of static assets to serve or a command to run."
188
+ }): Promise<void | number> {
189
+ if (command.length === 0) {
190
+ CLEANUP();
191
+ throw new FatalError(
192
+ "Must specify a directory of static assets to serve or a command to run.",
193
+ 1
91
194
  );
195
+ }
92
196
 
93
- console.log(`Running ${command.join(" ")}...`);
197
+ logger.log(`Running ${command.join(" ")}...`);
94
198
  const proxy = spawn(
95
199
  command[0].toString(),
96
200
  command.slice(1).map((value) => value.toString()),
@@ -102,27 +206,30 @@ const spawnProxyProcess = async ({
102
206
  },
103
207
  }
104
208
  );
209
+ CLEANUP_CALLBACKS.push(() => {
210
+ proxy.kill();
211
+ });
105
212
 
106
213
  proxy.stdout.on("data", (data) => {
107
- console.log(`[proxy]: ${data}`);
214
+ logger.log(`[proxy]: ${data}`);
108
215
  });
109
216
 
110
217
  proxy.stderr.on("data", (data) => {
111
- console.error(`[proxy]: ${data}`);
218
+ logger.error(`[proxy]: ${data}`);
112
219
  });
113
220
 
114
221
  proxy.on("close", (code) => {
115
- console.error(`Proxy exited with status ${code}.`);
222
+ logger.error(`Proxy exited with status ${code}.`);
116
223
  });
117
224
 
118
225
  // Wait for proxy process to start...
119
226
  while (!proxy.pid) {}
120
227
 
121
228
  if (port === undefined) {
122
- console.log(
229
+ logger.log(
123
230
  `Sleeping ${SECONDS_TO_WAIT_FOR_PROXY} seconds to allow proxy process to start before attempting to automatically determine port...`
124
231
  );
125
- console.log("To skip, specify the proxy port with --proxy.");
232
+ logger.log("To skip, specify the proxy port with --proxy.");
126
233
  await sleep(SECONDS_TO_WAIT_FOR_PROXY * 1000);
127
234
 
128
235
  port = getPids(proxy.pid)
@@ -130,34 +237,36 @@ const spawnProxyProcess = async ({
130
237
  .filter((port) => port !== undefined)[0];
131
238
 
132
239
  if (port === undefined) {
133
- return exit(
134
- "Could not automatically determine proxy port. Please specify the proxy port with --proxy."
240
+ CLEANUP();
241
+ throw new FatalError(
242
+ "Could not automatically determine proxy port. Please specify the proxy port with --proxy.",
243
+ 1
135
244
  );
136
245
  } else {
137
- console.log(`Automatically determined the proxy port to be ${port}.`);
246
+ logger.log(`Automatically determined the proxy port to be ${port}.`);
138
247
  }
139
248
  }
140
249
 
141
- return { port, exit };
142
- };
250
+ return port;
251
+ }
143
252
 
144
- const escapeRegex = (str: string) => {
253
+ function escapeRegex(str: string) {
145
254
  return str.replace(/[-/\\^$*+?.()|[]{}]/g, "\\$&");
146
- };
255
+ }
147
256
 
148
- export type Replacements = Record<string, string>;
257
+ type Replacements = Record<string, string>;
149
258
 
150
- export const replacer = (str: string, replacements: Replacements) => {
259
+ function replacer(str: string, replacements: Replacements) {
151
260
  for (const [replacement, value] of Object.entries(replacements)) {
152
261
  str = str.replace(`:${replacement}`, value);
153
262
  }
154
263
  return str;
155
- };
264
+ }
156
265
 
157
- export const generateRulesMatcher = <T,>(
266
+ function generateRulesMatcher<T>(
158
267
  rules?: Record<string, T>,
159
268
  replacer: (match: T, replacements: Replacements) => T = (match) => match
160
- ) => {
269
+ ) {
161
270
  // TODO: How can you test cross-host rules?
162
271
  if (!rules) return () => [];
163
272
 
@@ -191,7 +300,7 @@ export const generateRulesMatcher = <T,>(
191
300
  T
192
301
  ][];
193
302
 
194
- return ({ request }: { request: Request }) => {
303
+ return ({ request }: { request: MiniflareRequest }) => {
195
304
  const { pathname, host } = new URL(request.url);
196
305
 
197
306
  return compiledRules
@@ -204,9 +313,9 @@ export const generateRulesMatcher = <T,>(
204
313
  })
205
314
  .filter((value) => value !== undefined) as T[];
206
315
  };
207
- };
316
+ }
208
317
 
209
- const generateHeadersMatcher = (headersFile: string) => {
318
+ function generateHeadersMatcher(headersFile: string) {
210
319
  if (existsSync(headersFile)) {
211
320
  const contents = readFileSync(headersFile).toString();
212
321
 
@@ -264,7 +373,7 @@ const generateHeadersMatcher = (headersFile: string) => {
264
373
  )
265
374
  );
266
375
 
267
- return (request: Request) => {
376
+ return (request: MiniflareRequest) => {
268
377
  const matches = rulesMatcher({
269
378
  request,
270
379
  });
@@ -273,9 +382,9 @@ const generateHeadersMatcher = (headersFile: string) => {
273
382
  } else {
274
383
  return () => undefined;
275
384
  }
276
- };
385
+ }
277
386
 
278
- const generateRedirectsMatcher = (redirectsFile: string) => {
387
+ function generateRedirectsMatcher(redirectsFile: string) {
279
388
  if (existsSync(redirectsFile)) {
280
389
  const contents = readFileSync(redirectsFile).toString();
281
390
 
@@ -313,7 +422,7 @@ const generateRedirectsMatcher = (redirectsFile: string) => {
313
422
  })
314
423
  );
315
424
 
316
- return (request: Request) => {
425
+ return (request: MiniflareRequest) => {
317
426
  const match = rulesMatcher({
318
427
  request,
319
428
  })[0];
@@ -322,26 +431,26 @@ const generateRedirectsMatcher = (redirectsFile: string) => {
322
431
  } else {
323
432
  return () => undefined;
324
433
  }
325
- };
434
+ }
326
435
 
327
- const extractPathname = (
436
+ function extractPathname(
328
437
  path = "/",
329
438
  includeSearch: boolean,
330
439
  includeHash: boolean
331
- ) => {
440
+ ) {
332
441
  if (!path.startsWith("/")) path = `/${path}`;
333
442
  const url = new URL(`//${path}`, "relative://");
334
443
  return `${url.pathname}${includeSearch ? url.search : ""}${
335
444
  includeHash ? url.hash : ""
336
445
  }`;
337
- };
446
+ }
338
447
 
339
- const validateURL = (
448
+ function validateURL(
340
449
  token: string,
341
450
  onlyRelative = false,
342
451
  includeSearch = false,
343
452
  includeHash = false
344
- ) => {
453
+ ) {
345
454
  const host = /^https:\/\/+(?<host>[^/]+)\/?(?<path>.*)/.exec(token);
346
455
  if (host && host.groups && host.groups.host) {
347
456
  if (onlyRelative) return;
@@ -362,14 +471,18 @@ const validateURL = (
362
471
  }
363
472
  }
364
473
  return "";
365
- };
474
+ }
366
475
 
367
- const hasFileExtension = (pathname: string) =>
368
- /\/.+\.[a-z0-9]+$/i.test(pathname);
476
+ function hasFileExtension(pathname: string) {
477
+ return /\/.+\.[a-z0-9]+$/i.test(pathname);
478
+ }
369
479
 
370
- const generateAssetsFetch = async (
480
+ async function generateAssetsFetch(
371
481
  directory: string
372
- ): Promise<typeof fetch> => {
482
+ ): Promise<typeof miniflareFetch> {
483
+ // Defer importing miniflare until we really need it
484
+ const { Headers, Request, Response } = await import("@miniflare/core");
485
+
373
486
  const headersFile = join(directory, "_headers");
374
487
  const redirectsFile = join(directory, "_redirects");
375
488
  const workerFile = join(directory, "_worker.js");
@@ -399,12 +512,12 @@ const generateAssetsFetch = async (
399
512
  }).on("change", (path) => {
400
513
  switch (path) {
401
514
  case headersFile: {
402
- console.log("_headers modified. Re-evaluating...");
515
+ logger.log("_headers modified. Re-evaluating...");
403
516
  headersMatcher = generateHeadersMatcher(headersFile);
404
517
  break;
405
518
  }
406
519
  case redirectsFile: {
407
- console.log("_redirects modified. Re-evaluating...");
520
+ logger.log("_redirects modified. Re-evaluating...");
408
521
  redirectsMatcher = generateRedirectsMatcher(redirectsFile);
409
522
  break;
410
523
  }
@@ -415,12 +528,12 @@ const generateAssetsFetch = async (
415
528
  return readFileSync(file);
416
529
  };
417
530
 
418
- const generateResponse = (request: Request) => {
531
+ const generateResponse = (request: MiniflareRequest) => {
419
532
  const url = new URL(request.url);
420
533
 
421
534
  const deconstructedResponse: {
422
535
  status: number;
423
- headers: Headers;
536
+ headers: MiniflareHeaders;
424
537
  body?: Buffer;
425
538
  } = {
426
539
  status: 200,
@@ -466,14 +579,25 @@ const generateAssetsFetch = async (
466
579
  if ((asset = getAsset(`${cwd}/404.html`))) {
467
580
  deconstructedResponse.status = 404;
468
581
  deconstructedResponse.body = serveAsset(asset);
582
+ deconstructedResponse.headers.set(
583
+ "Content-Type",
584
+ getType(asset) || "application/octet-stream"
585
+ );
469
586
  return deconstructedResponse;
470
587
  }
471
588
  }
472
589
 
473
590
  if ((asset = getAsset(`/index.html`))) {
474
591
  deconstructedResponse.body = serveAsset(asset);
592
+ deconstructedResponse.headers.set(
593
+ "Content-Type",
594
+ getType(asset) || "application/octet-stream"
595
+ );
475
596
  return deconstructedResponse;
476
597
  }
598
+
599
+ deconstructedResponse.status = 404;
600
+ return deconstructedResponse;
477
601
  };
478
602
 
479
603
  let asset;
@@ -481,6 +605,10 @@ const generateAssetsFetch = async (
481
605
  if (url.pathname.endsWith("/")) {
482
606
  if ((asset = getAsset(`${url.pathname}/index.html`))) {
483
607
  deconstructedResponse.body = serveAsset(asset);
608
+ deconstructedResponse.headers.set(
609
+ "Content-Type",
610
+ getType(asset) || "application/octet-stream"
611
+ );
484
612
  return deconstructedResponse;
485
613
  } else if (
486
614
  (asset = getAsset(`${url.pathname.replace(/\/$/, ".html")}`))
@@ -508,6 +636,10 @@ const generateAssetsFetch = async (
508
636
  const extensionlessPath = url.pathname.slice(0, -".html".length);
509
637
  if (getAsset(extensionlessPath) || extensionlessPath === "/") {
510
638
  deconstructedResponse.body = serveAsset(asset);
639
+ deconstructedResponse.headers.set(
640
+ "Content-Type",
641
+ getType(asset) || "application/octet-stream"
642
+ );
511
643
  return deconstructedResponse;
512
644
  } else {
513
645
  deconstructedResponse.status = 301;
@@ -519,6 +651,10 @@ const generateAssetsFetch = async (
519
651
  }
520
652
  } else {
521
653
  deconstructedResponse.body = serveAsset(asset);
654
+ deconstructedResponse.headers.set(
655
+ "Content-Type",
656
+ getType(asset) || "application/octet-stream"
657
+ );
522
658
  return deconstructedResponse;
523
659
  }
524
660
  } else if (hasFileExtension(url.pathname)) {
@@ -528,6 +664,10 @@ const generateAssetsFetch = async (
528
664
 
529
665
  if ((asset = getAsset(`${url.pathname}.html`))) {
530
666
  deconstructedResponse.body = serveAsset(asset);
667
+ deconstructedResponse.headers.set(
668
+ "Content-Type",
669
+ getType(asset) || "application/octet-stream"
670
+ );
531
671
  return deconstructedResponse;
532
672
  }
533
673
 
@@ -545,8 +685,12 @@ const generateAssetsFetch = async (
545
685
  };
546
686
 
547
687
  const attachHeaders = (
548
- request: Request,
549
- deconstructedResponse: { status: number; headers: Headers; body?: Buffer }
688
+ request: MiniflareRequest,
689
+ deconstructedResponse: {
690
+ status: number;
691
+ headers: MiniflareHeaders;
692
+ body?: Buffer;
693
+ }
550
694
  ) => {
551
695
  const headers = deconstructedResponse.headers;
552
696
  const newHeaders = new Headers({});
@@ -569,7 +713,7 @@ const generateAssetsFetch = async (
569
713
  });
570
714
  };
571
715
 
572
- return (async (input, init) => {
716
+ return async (input, init) => {
573
717
  const request = new Request(input, init);
574
718
  const deconstructedResponse = generateResponse(request);
575
719
  attachHeaders(request, deconstructedResponse);
@@ -584,212 +728,873 @@ const generateAssetsFetch = async (
584
728
  headers,
585
729
  status: deconstructedResponse.status,
586
730
  });
587
- }) as any;
588
- };
731
+ };
732
+ }
733
+
734
+ const RUNNING_BUILDERS: BuildResult[] = [];
735
+
736
+ async function buildFunctions({
737
+ outfile,
738
+ outputConfigPath,
739
+ functionsDirectory,
740
+ minify = false,
741
+ sourcemap = false,
742
+ fallbackService = "ASSETS",
743
+ watch = false,
744
+ onEnd,
745
+ plugin = false,
746
+ buildOutputDirectory,
747
+ nodeCompat,
748
+ }: {
749
+ outfile: string;
750
+ outputConfigPath?: string;
751
+ functionsDirectory: string;
752
+ minify?: boolean;
753
+ sourcemap?: boolean;
754
+ fallbackService?: string;
755
+ watch?: boolean;
756
+ onEnd?: () => void;
757
+ plugin?: boolean;
758
+ buildOutputDirectory?: string;
759
+ nodeCompat?: boolean;
760
+ }) {
761
+ RUNNING_BUILDERS.forEach(
762
+ (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
763
+ );
589
764
 
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", {
765
+ const routesModule = join(tmpdir(), `./functionsRoutes-${Math.random()}.mjs`);
766
+ const baseURL = toUrlPath("/");
767
+
768
+ const config: Config = await generateConfigFromFileTree({
769
+ baseDir: functionsDirectory,
770
+ baseURL,
771
+ });
772
+
773
+ if (outputConfigPath) {
774
+ writeFileSync(
775
+ outputConfigPath,
776
+ JSON.stringify({ ...config, baseURL }, null, 2)
777
+ );
778
+ }
779
+
780
+ await writeRoutesModule({
781
+ config,
782
+ srcDir: functionsDirectory,
783
+ outfile: routesModule,
784
+ });
785
+
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;
822
+ }
823
+
824
+ const createDeployment: CommandModule<
825
+ CreateDeploymentArgs,
826
+ CreateDeploymentArgs
827
+ > = {
828
+ describe: "🆙 Publish a directory of static assets as a Pages deployment",
829
+ builder: (yargs) => {
830
+ return yargs
831
+ .positional("directory", {
832
+ type: "string",
833
+ demandOption: true,
834
+ description: "The directory of static files to upload",
835
+ })
836
+ .options({
837
+ "project-name": {
597
838
  type: "string",
598
- demandOption: undefined,
599
- description: "The directory of static assets to serve",
600
- })
601
- .positional("command", {
839
+ description: "The name of the project you want to deploy to",
840
+ },
841
+ branch: {
602
842
  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
843
+ description: "The name of the branch you want to deploy to",
844
+ },
845
+ "commit-hash": {
846
+ type: "string",
847
+ description: "The SHA to attach to this deployment",
848
+ },
849
+ "commit-message": {
850
+ type: "string",
851
+ description: "The commit message to attach to this deployment",
852
+ },
853
+ "commit-dirty": {
854
+ type: "boolean",
855
+ description:
856
+ "Whether or not the workspace should be considered dirty for this deployment",
857
+ },
858
+ })
859
+ .epilogue(pagesBetaWarning);
860
+ },
861
+ handler: async ({
862
+ directory,
863
+ projectName,
864
+ branch,
865
+ commitHash,
866
+ commitMessage,
867
+ commitDirty,
868
+ }) => {
869
+ if (!directory) {
870
+ throw new FatalError("Must specify a directory.", 1);
871
+ }
872
+
873
+ const config = getConfigCache<PagesConfigCache>(
874
+ PAGES_CONFIG_CACHE_FILENAME
875
+ );
876
+ const accountId = await requireAuth(config);
877
+
878
+ projectName ??= config.project_name;
879
+
880
+ const isInteractive = process.stdin.isTTY;
881
+ if (!projectName && isInteractive) {
882
+ const projects = (await listProjects({ accountId })).filter(
883
+ (project) => !project.source
884
+ );
885
+
886
+ let existingOrNew: "existing" | "new" = "new";
887
+
888
+ if (projects.length > 0) {
889
+ existingOrNew = await new Promise<"new" | "existing">((resolve) => {
890
+ const { unmount } = render(
891
+ <>
892
+ <Text>
893
+ No project selected. Would you like to create one or use an
894
+ existing project?
895
+ </Text>
896
+ <SelectInput
897
+ items={[
898
+ {
899
+ key: "new",
900
+ label: "Create a new project",
901
+ value: "new",
902
+ },
903
+ {
904
+ key: "existing",
905
+ label: "Use an existing project",
906
+ value: "existing",
907
+ },
908
+ ]}
909
+ onSelect={async (selected) => {
910
+ resolve(selected.value as "new" | "existing");
911
+ unmount();
912
+ }}
913
+ />
914
+ </>
915
+ );
644
916
  });
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
917
  }
661
918
 
662
- const functionsDirectory = "./functions";
663
- const usingFunctions = existsSync(functionsDirectory);
919
+ switch (existingOrNew) {
920
+ case "existing": {
921
+ projectName = await new Promise((resolve) => {
922
+ const { unmount } = render(
923
+ <>
924
+ <Text>Select a project:</Text>
925
+ <SelectInput
926
+ items={projects.map((project) => ({
927
+ key: project.name,
928
+ label: project.name,
929
+ value: project,
930
+ }))}
931
+ onSelect={async (selected) => {
932
+ resolve(selected.value.name);
933
+ unmount();
934
+ }}
935
+ />
936
+ </>
937
+ );
938
+ });
939
+ break;
940
+ }
941
+ case "new": {
942
+ projectName = await prompt("Enter the name of your new project:");
664
943
 
665
- const command = remaining as (string | number)[];
944
+ if (!projectName) {
945
+ throw new FatalError("Must specify a project name.", 1);
946
+ }
666
947
 
667
- let proxyPort: number | undefined;
668
- let exit: Exit = (message) => {
669
- console.error(message);
670
- return undefined;
671
- };
948
+ let isGitDir = true;
949
+ try {
950
+ execSync(`git rev-parse --is-inside-work-tree`, {
951
+ stdio: "ignore",
952
+ });
953
+ } catch (err) {
954
+ isGitDir = false;
955
+ }
672
956
 
673
- if (directory === undefined) {
674
- const proxy = await spawnProxyProcess({
675
- port: requestedProxyPort,
676
- command,
677
- });
678
- if (proxy === undefined) return undefined;
957
+ const productionBranch = await prompt(
958
+ "Enter the production branch name:",
959
+ "text",
960
+ isGitDir
961
+ ? execSync(`git rev-parse --abbrev-ref HEAD`).toString().trim()
962
+ : "production"
963
+ );
679
964
 
680
- exit = proxy.exit;
681
- proxyPort = proxy.port;
965
+ if (!productionBranch) {
966
+ throw new FatalError("Must specify a production branch.", 1);
967
+ }
682
968
 
683
- process.on("SIGINT", () => exit());
684
- process.on("SIGTERM", () => exit());
969
+ await fetchResult<Project>(`/accounts/${accountId}/pages/projects`, {
970
+ method: "POST",
971
+ body: JSON.stringify({
972
+ name: projectName,
973
+ production_branch: productionBranch,
974
+ }),
975
+ });
976
+
977
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
978
+ account_id: accountId,
979
+ project_name: projectName,
980
+ });
981
+
982
+ logger.log(`✨ Successfully created the '${projectName}' project.`);
983
+ break;
984
+ }
685
985
  }
986
+ }
686
987
 
687
- let miniflareArgs: MiniflareOptions = {};
988
+ if (!projectName) {
989
+ throw new FatalError("Must specify a project name.", 1);
990
+ }
688
991
 
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;
992
+ // We infer git info by default is not passed in
701
993
 
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
- );
994
+ let isGitDir = true;
995
+ try {
996
+ execSync(`git rev-parse --is-inside-work-tree`, {
997
+ stdio: "ignore",
998
+ });
999
+ } catch (err) {
1000
+ isGitDir = false;
1001
+ }
1002
+
1003
+ let isGitDirty = false;
1004
+
1005
+ if (isGitDir) {
1006
+ try {
1007
+ isGitDirty = Boolean(
1008
+ execSync(`git status --porcelain`).toString().length
1009
+ );
1010
+
1011
+ if (!branch) {
1012
+ branch = execSync(`git rev-parse --abbrev-ref HEAD`)
1013
+ .toString()
1014
+ .trim();
1015
+ }
1016
+
1017
+ if (!commitHash) {
1018
+ commitHash = execSync(`git rev-parse HEAD`).toString().trim();
1019
+ }
1020
+
1021
+ if (!commitMessage) {
1022
+ commitMessage = execSync(`git show -s --format=%B ${commitHash}`)
1023
+ .toString()
1024
+ .trim();
706
1025
  }
1026
+ } catch (err) {}
707
1027
 
708
- miniflareArgs = {
709
- scriptPath,
710
- };
1028
+ if (isGitDirty && !commitDirty) {
1029
+ logger.warn(
1030
+ `Warning: Your working directory is a git repo and has uncommitted changes\nTo silence this warning, pass in --commit-dirty=true`
1031
+ );
711
1032
  }
712
1033
 
713
- const { Miniflare, Log, LogLevel } = await import("miniflare");
714
- const { fetch } = await import("@miniflare/core");
1034
+ if (commitDirty === undefined) {
1035
+ commitDirty = isGitDirty;
1036
+ }
1037
+ }
715
1038
 
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;
742
- }
1039
+ let builtFunctions: string | undefined = undefined;
1040
+ const functionsDirectory = join(cwd(), "functions");
1041
+ if (existsSync(functionsDirectory)) {
1042
+ const outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`);
1043
+
1044
+ await new Promise((resolve) =>
1045
+ buildFunctions({
1046
+ outfile,
1047
+ functionsDirectory,
1048
+ onEnd: () => resolve(null),
1049
+ buildOutputDirectory: dirname(outfile),
1050
+ })
1051
+ );
1052
+
1053
+ builtFunctions = readFileSync(outfile, "utf-8");
1054
+ }
1055
+
1056
+ type FileContainer = {
1057
+ content: string;
1058
+ contentType: string;
1059
+ sizeInBytes: number;
1060
+ hash: string;
1061
+ };
1062
+
1063
+ const IGNORE_LIST = [
1064
+ "_worker.js",
1065
+ "_redirects",
1066
+ "_headers",
1067
+ ".DS_Store",
1068
+ "node_modules",
1069
+ ".git",
1070
+ ];
1071
+
1072
+ const walk = async (
1073
+ dir: string,
1074
+ fileMap: Map<string, FileContainer> = new Map(),
1075
+ depth = 0
1076
+ ) => {
1077
+ const files = await readdir(dir);
1078
+
1079
+ await Promise.all(
1080
+ files.map(async (file) => {
1081
+ const filepath = join(dir, file);
1082
+ const filestat = await stat(filepath);
1083
+
1084
+ if (IGNORE_LIST.includes(file)) {
1085
+ return;
1086
+ }
1087
+
1088
+ if (filestat.isSymbolicLink()) {
1089
+ return;
1090
+ }
1091
+
1092
+ if (filestat.isDirectory()) {
1093
+ fileMap = await walk(filepath, fileMap, depth + 1);
1094
+ } else {
1095
+ let name;
1096
+ if (depth) {
1097
+ name = filepath.split(sep).slice(1).join("/");
1098
+ } else {
1099
+ name = file;
1100
+ }
1101
+
1102
+ // TODO: Move this to later so we don't hold as much in memory
1103
+ const fileContent = await readFile(filepath);
1104
+
1105
+ const base64Content = fileContent.toString("base64");
1106
+ const extension = extname(basename(name)).substring(1);
1107
+
1108
+ if (filestat.size > 25 * 1024 * 1024) {
1109
+ throw new Error(
1110
+ `Error: Pages only supports files up to ${prettyBytes(
1111
+ 25 * 1024 * 1024
1112
+ )} in size\n${name} is ${prettyBytes(filestat.size)} in size`
1113
+ );
743
1114
  }
1115
+
1116
+ fileMap.set(name, {
1117
+ content: base64Content,
1118
+ contentType: getType(name) || "application/octet-stream",
1119
+ sizeInBytes: filestat.size,
1120
+ hash: hash(base64Content + extension)
1121
+ .toString("hex")
1122
+ .slice(0, 32),
1123
+ });
744
1124
  }
1125
+ })
1126
+ );
1127
+
1128
+ return fileMap;
1129
+ };
1130
+
1131
+ const fileMap = await walk(directory);
1132
+
1133
+ if (fileMap.size > 20000) {
1134
+ throw new FatalError(
1135
+ `Error: Pages only supports up to 20,000 files in a deployment. Ensure you have specified your build output directory correctly.`,
1136
+ 1
1137
+ );
1138
+ }
1139
+
1140
+ const files = [...fileMap.values()];
1141
+
1142
+ const { jwt } = await fetchResult<{ jwt: string }>(
1143
+ `/accounts/${accountId}/pages/projects/${projectName}/upload-token`
1144
+ );
1145
+
1146
+ const start = Date.now();
1147
+
1148
+ const missingHashes = await fetchResult<string[]>(
1149
+ `/pages/assets/check-missing`,
1150
+ {
1151
+ method: "POST",
1152
+ headers: {
1153
+ "Content-Type": "application/json",
1154
+ Authorization: `Bearer ${jwt}`,
1155
+ },
1156
+ body: JSON.stringify({
1157
+ hashes: files.map(({ hash }) => hash),
1158
+ }),
1159
+ }
1160
+ );
1161
+
1162
+ const sortedFiles = files
1163
+ .filter((file) => missingHashes.includes(file.hash))
1164
+ .sort((a, b) => b.sizeInBytes - a.sizeInBytes);
1165
+
1166
+ // Start with a few buckets so small projects still get
1167
+ // the benefit of multiple upload streams
1168
+ const buckets: {
1169
+ files: FileContainer[];
1170
+ remainingSize: number;
1171
+ }[] = new Array(BULK_UPLOAD_CONCURRENCY).fill(null).map(() => ({
1172
+ files: [],
1173
+ remainingSize: MAX_BUCKET_SIZE,
1174
+ }));
1175
+
1176
+ let bucketOffset = 0;
1177
+ for (const file of sortedFiles) {
1178
+ let inserted = false;
1179
+
1180
+ for (let i = 0; i < buckets.length; i++) {
1181
+ // Start at a different bucket for each new file
1182
+ const bucket = buckets[(i + bucketOffset) % buckets.length];
1183
+ if (
1184
+ bucket.remainingSize >= file.sizeInBytes &&
1185
+ bucket.files.length < MAX_BUCKET_FILE_COUNT
1186
+ ) {
1187
+ bucket.files.push(file);
1188
+ bucket.remainingSize -= file.sizeInBytes;
1189
+ inserted = true;
1190
+ break;
745
1191
  }
1192
+ }
746
1193
 
747
- log(message: string) {}
1194
+ if (!inserted) {
1195
+ buckets.push({
1196
+ files: [file],
1197
+ remainingSize: MAX_BUCKET_SIZE - file.sizeInBytes,
1198
+ });
1199
+ }
1200
+ bucketOffset++;
1201
+ }
748
1202
 
749
- error(message: Error) {
750
- this.logWithLevel(
751
- LogLevel.ERROR,
752
- message.stack ? message.stack : `${message}`
753
- );
1203
+ let counter = fileMap.size - sortedFiles.length;
1204
+ const { rerender, unmount } = render(
1205
+ <Progress done={counter} total={fileMap.size} />
1206
+ );
1207
+
1208
+ const queue = new PQueue({ concurrency: BULK_UPLOAD_CONCURRENCY });
1209
+
1210
+ for (const bucket of buckets) {
1211
+ // Don't upload empty buckets (can happen for tiny projects)
1212
+ if (bucket.files.length === 0) continue;
1213
+
1214
+ const payload: UploadPayloadFile[] = bucket.files.map((file) => ({
1215
+ key: file.hash,
1216
+ value: file.content,
1217
+ metadata: {
1218
+ contentType: file.contentType,
1219
+ },
1220
+ base64: true,
1221
+ }));
1222
+
1223
+ let attempts = 0;
1224
+ const doUpload = async (): Promise<void> => {
1225
+ try {
1226
+ return await fetchResult(`/pages/assets/upload`, {
1227
+ method: "POST",
1228
+ headers: {
1229
+ "Content-Type": "application/json",
1230
+ Authorization: `Bearer ${jwt}`,
1231
+ },
1232
+ body: JSON.stringify(payload),
1233
+ });
1234
+ } catch (e) {
1235
+ if (attempts < MAX_UPLOAD_ATTEMPTS) {
1236
+ // Linear backoff, 0 second first time, then 1 second etc.
1237
+ await new Promise((resolve) =>
1238
+ setTimeout(resolve, attempts++ * 1000)
1239
+ );
1240
+ return doUpload();
1241
+ } else {
1242
+ throw e;
1243
+ }
754
1244
  }
1245
+ };
1246
+
1247
+ queue.add(() =>
1248
+ doUpload().then(
1249
+ () => {
1250
+ counter += bucket.files.length;
1251
+ rerender(<Progress done={counter} total={fileMap.size} />);
1252
+ },
1253
+ (error) => {
1254
+ return Promise.reject(
1255
+ new FatalError(
1256
+ "Failed to upload files. Please try again.",
1257
+ error.code || 1
1258
+ )
1259
+ );
1260
+ }
1261
+ )
1262
+ );
1263
+ }
1264
+
1265
+ await queue.onIdle();
1266
+
1267
+ unmount();
1268
+
1269
+ const uploadMs = Date.now() - start;
1270
+
1271
+ logger.log(
1272
+ `✨ Success! Uploaded ${fileMap.size} files ${formatTime(uploadMs)}\n`
1273
+ );
1274
+
1275
+ const formData = new FormData();
1276
+
1277
+ formData.append(
1278
+ "manifest",
1279
+ JSON.stringify(
1280
+ Object.fromEntries(
1281
+ [...fileMap.entries()].map(([fileName, file]) => [
1282
+ `/${fileName}`,
1283
+ file.hash,
1284
+ ])
1285
+ )
1286
+ )
1287
+ );
1288
+
1289
+ if (branch) {
1290
+ formData.append("branch", branch);
1291
+ }
1292
+
1293
+ if (commitMessage) {
1294
+ formData.append("commit_message", commitMessage);
1295
+ }
1296
+
1297
+ if (commitHash) {
1298
+ formData.append("commit_hash", commitHash);
1299
+ }
1300
+
1301
+ if (commitDirty !== undefined) {
1302
+ formData.append("commit_dirty", commitDirty);
1303
+ }
1304
+
1305
+ let _headers: string | undefined,
1306
+ _redirects: string | undefined,
1307
+ _workerJS: string | undefined;
1308
+
1309
+ try {
1310
+ _headers = readFileSync(join(directory, "_headers"), "utf-8");
1311
+ } catch {}
1312
+
1313
+ try {
1314
+ _redirects = readFileSync(join(directory, "_redirects"), "utf-8");
1315
+ } catch {}
1316
+
1317
+ try {
1318
+ _workerJS = readFileSync(join(directory, "_worker.js"), "utf-8");
1319
+ } catch {}
1320
+
1321
+ if (_headers) {
1322
+ formData.append("_headers", new File([_headers], "_headers"));
1323
+ }
1324
+
1325
+ if (_redirects) {
1326
+ formData.append("_redirects", new File([_redirects], "_redirects"));
1327
+ }
1328
+
1329
+ if (builtFunctions) {
1330
+ formData.append("_worker.js", new File([builtFunctions], "_worker.js"));
1331
+ } else if (_workerJS) {
1332
+ formData.append("_worker.js", new File([_workerJS], "_worker.js"));
1333
+ }
1334
+
1335
+ const deploymentResponse = await fetchResult<Deployment>(
1336
+ `/accounts/${accountId}/pages/projects/${projectName}/deployments`,
1337
+ {
1338
+ method: "POST",
1339
+ body: formData,
755
1340
  }
1341
+ );
1342
+
1343
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1344
+ account_id: accountId,
1345
+ project_name: projectName,
1346
+ });
1347
+
1348
+ logger.log(
1349
+ `✨ Deployment complete! Take a peek over at ${deploymentResponse.url}`
1350
+ );
1351
+ },
1352
+ };
756
1353
 
757
- const miniflare = new Miniflare({
1354
+ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1355
+ return yargs
1356
+ .command(
1357
+ "dev [directory] [-- command..]",
1358
+ "🧑‍💻 Develop your full-stack Pages application locally",
1359
+ (yargs) => {
1360
+ return yargs
1361
+ .positional("directory", {
1362
+ type: "string",
1363
+ demandOption: undefined,
1364
+ description: "The directory of static assets to serve",
1365
+ })
1366
+ .positional("command", {
1367
+ type: "string",
1368
+ demandOption: undefined,
1369
+ description: "The proxy command to run",
1370
+ })
1371
+ .options({
1372
+ local: {
1373
+ type: "boolean",
1374
+ default: true,
1375
+ description: "Run on my machine",
1376
+ },
1377
+ port: {
1378
+ type: "number",
1379
+ default: 8788,
1380
+ description: "The port to listen on (serve from)",
1381
+ },
1382
+ proxy: {
1383
+ type: "number",
1384
+ description:
1385
+ "The port to proxy (where the static assets are served)",
1386
+ },
1387
+ "script-path": {
1388
+ type: "string",
1389
+ default: "_worker.js",
1390
+ description:
1391
+ "The location of the single Worker script if not using functions",
1392
+ },
1393
+ binding: {
1394
+ type: "array",
1395
+ description: "Bind variable/secret (KEY=VALUE)",
1396
+ alias: "b",
1397
+ },
1398
+ kv: {
1399
+ type: "array",
1400
+ description: "KV namespace to bind",
1401
+ alias: "k",
1402
+ },
1403
+ do: {
1404
+ type: "array",
1405
+ description: "Durable Object to bind (NAME=CLASS)",
1406
+ alias: "o",
1407
+ },
1408
+ "live-reload": {
1409
+ type: "boolean",
1410
+ default: false,
1411
+ description: "Auto reload HTML pages when change is detected",
1412
+ },
1413
+ "node-compat": {
1414
+ describe: "Enable node.js compatibility",
1415
+ default: false,
1416
+ type: "boolean",
1417
+ hidden: true,
1418
+ },
1419
+ // TODO: Miniflare user options
1420
+ })
1421
+ .epilogue(pagesBetaWarning);
1422
+ },
1423
+ async ({
1424
+ local,
1425
+ directory,
758
1426
  port,
759
- watch: true,
760
- modules: true,
1427
+ proxy: requestedProxyPort,
1428
+ "script-path": singleWorkerScriptPath,
1429
+ binding: bindings = [],
1430
+ kv: kvs = [],
1431
+ do: durableObjects = [],
1432
+ "live-reload": liveReload,
1433
+ "node-compat": nodeCompat,
1434
+ _: [_pages, _dev, ...remaining],
1435
+ }) => {
1436
+ // Beta message for `wrangler pages <commands>` usage
1437
+ logger.log(pagesBetaWarning);
1438
+
1439
+ if (!local) {
1440
+ logger.error("Only local mode is supported at the moment.");
1441
+ return;
1442
+ }
761
1443
 
762
- log: new MiniflareLogger(LogLevel.ERROR),
1444
+ const functionsDirectory = "./functions";
1445
+ const usingFunctions = existsSync(functionsDirectory);
763
1446
 
764
- kvNamespaces: kvs.map((kv) => kv.toString()),
1447
+ const command = remaining as (string | number)[];
765
1448
 
766
- durableObjects: Object.fromEntries(
767
- durableObjects.map((durableObject) =>
768
- durableObject.toString().split("=")
769
- )
770
- ),
1449
+ let proxyPort: number | void;
771
1450
 
772
- bindings: {
773
- // User bindings
774
- ...Object.fromEntries(
775
- bindings.map((binding) => binding.toString().split("="))
1451
+ if (directory === undefined) {
1452
+ proxyPort = await spawnProxyProcess({
1453
+ port: requestedProxyPort,
1454
+ command,
1455
+ });
1456
+ if (proxyPort === undefined) return undefined;
1457
+ }
1458
+
1459
+ let miniflareArgs: MiniflareOptions = {};
1460
+
1461
+ let scriptReadyResolve: () => void;
1462
+ const scriptReadyPromise = new Promise<void>(
1463
+ (resolve) => (scriptReadyResolve = resolve)
1464
+ );
1465
+
1466
+ if (usingFunctions) {
1467
+ const outfile = join(
1468
+ tmpdir(),
1469
+ `./functionsWorker-${Math.random()}.js`
1470
+ );
1471
+
1472
+ if (nodeCompat) {
1473
+ console.warn(
1474
+ "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."
1475
+ );
1476
+ }
1477
+
1478
+ logger.log(`Compiling worker to "${outfile}"...`);
1479
+
1480
+ try {
1481
+ await buildFunctions({
1482
+ outfile,
1483
+ functionsDirectory,
1484
+ sourcemap: true,
1485
+ watch: true,
1486
+ onEnd: () => scriptReadyResolve(),
1487
+ buildOutputDirectory: directory,
1488
+ nodeCompat,
1489
+ });
1490
+ } catch {}
1491
+
1492
+ watch([functionsDirectory], {
1493
+ persistent: true,
1494
+ ignoreInitial: true,
1495
+ }).on("all", async () => {
1496
+ await buildFunctions({
1497
+ outfile,
1498
+ functionsDirectory,
1499
+ sourcemap: true,
1500
+ watch: true,
1501
+ onEnd: () => scriptReadyResolve(),
1502
+ buildOutputDirectory: directory,
1503
+ nodeCompat,
1504
+ });
1505
+ });
1506
+
1507
+ miniflareArgs = {
1508
+ scriptPath: outfile,
1509
+ };
1510
+ } else {
1511
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1512
+ scriptReadyResolve!();
1513
+
1514
+ const scriptPath =
1515
+ directory !== undefined
1516
+ ? join(directory, singleWorkerScriptPath)
1517
+ : singleWorkerScriptPath;
1518
+
1519
+ if (existsSync(scriptPath)) {
1520
+ miniflareArgs = {
1521
+ scriptPath,
1522
+ };
1523
+ } else {
1524
+ logger.log("No functions. Shimming...");
1525
+ miniflareArgs = {
1526
+ // cfFetch sets the `cf` object that a function could expect
1527
+ // If there are no functions, there's no reason to set this up (and not make that network call)
1528
+ cfFetch: false,
1529
+ // TODO: The fact that these request/response hacks are necessary is ridiculous.
1530
+ // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
1531
+ script: `
1532
+ export default {
1533
+ async fetch(request, env, context) {
1534
+ const response = await env.ASSETS.fetch(request.url, request)
1535
+ return new Response(response.body, response)
1536
+ }
1537
+ }`,
1538
+ };
1539
+ }
1540
+ }
1541
+
1542
+ // Defer importing miniflare until we really need it
1543
+ const { Miniflare, Log, LogLevel } = await import("miniflare");
1544
+ const { Response, fetch } = await import("@miniflare/core");
1545
+
1546
+ // Wait for esbuild to finish building before starting Miniflare.
1547
+ // This must be before the call to `new Miniflare`, as that will
1548
+ // asynchronously start loading the script. `await startServer()`
1549
+ // internally just waits for that promise to resolve.
1550
+ await scriptReadyPromise;
1551
+
1552
+ // `assetsFetch()` will only be called if there is `proxyPort` defined.
1553
+ // We only define `proxyPort`, above, when there is no `directory` defined.
1554
+ const assetsFetch =
1555
+ directory !== undefined
1556
+ ? await generateAssetsFetch(directory)
1557
+ : invalidAssetsFetch;
1558
+
1559
+ const requestContextCheckOptions =
1560
+ await getRequestContextCheckOptions();
1561
+
1562
+ const miniflare = new Miniflare({
1563
+ port,
1564
+ watch: true,
1565
+ modules: true,
1566
+
1567
+ log: new Log(LogLevel.ERROR, { prefix: "pages" }),
1568
+ logUnhandledRejections: true,
1569
+ sourceMap: true,
1570
+
1571
+ kvNamespaces: kvs.map((kv) => kv.toString()),
1572
+
1573
+ durableObjects: Object.fromEntries(
1574
+ durableObjects.map((durableObject) =>
1575
+ durableObject.toString().split("=")
1576
+ )
776
1577
  ),
777
1578
 
1579
+ // User bindings
1580
+ bindings: {
1581
+ ...Object.fromEntries(
1582
+ bindings
1583
+ .map((binding) => binding.toString().split("="))
1584
+ .map(([key, ...values]) => [key, values.join("=")])
1585
+ ),
1586
+ },
1587
+
778
1588
  // env.ASSETS.fetch
779
- ASSETS: {
780
- fetch: async (
781
- input: RequestInfo,
782
- init?: RequestInit | undefined
783
- ) => {
1589
+ serviceBindings: {
1590
+ async ASSETS(request: MiniflareRequest) {
784
1591
  if (proxyPort) {
785
1592
  try {
786
- let request = new Request(input as any, init);
787
1593
  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);
1594
+ url.host = `localhost:${proxyPort}`;
1595
+ return await fetch(url, request);
791
1596
  } catch (thrown) {
792
- console.error(`Could not proxy request: ${thrown}`);
1597
+ logger.error(`Could not proxy request: ${thrown}`);
793
1598
 
794
1599
  // TODO: Pretty error page
795
1600
  return new Response(
@@ -799,11 +1604,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
799
1604
  }
800
1605
  } else {
801
1606
  try {
802
- return await (
803
- await generateAssetsFetch(directory)
804
- )(input as any, init as any);
1607
+ return await assetsFetch(request);
805
1608
  } catch (thrown) {
806
- console.error(`Could not serve static asset: ${thrown}`);
1609
+ logger.error(`Could not serve static asset: ${thrown}`);
807
1610
 
808
1611
  // TODO: Pretty error page
809
1612
  return new Response(
@@ -814,30 +1617,411 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
814
1617
  }
815
1618
  },
816
1619
  },
817
- },
818
1620
 
819
- kvPersist: true,
820
- durableObjectsPersist: true,
821
- cachePersist: true,
1621
+ kvPersist: true,
1622
+ durableObjectsPersist: true,
1623
+ cachePersist: true,
1624
+ liveReload,
822
1625
 
823
- ...miniflareArgs,
824
- });
1626
+ ...requestContextCheckOptions,
1627
+ ...miniflareArgs,
1628
+ });
825
1629
 
826
- const server = await miniflare.startServer();
827
- console.log(`Serving at http://127.0.0.1:${port}/`);
1630
+ try {
1631
+ // `startServer` might throw if user code contains errors
1632
+ const server = await miniflare.startServer();
1633
+ logger.log(`Serving at http://localhost:${port}/`);
828
1634
 
829
- if (process.env.BROWSER !== "none") {
830
- open(`http://127.0.0.1:${port}/`);
1635
+ if (process.env.BROWSER !== "none") {
1636
+ await openInBrowser(`http://localhost:${port}/`);
1637
+ }
1638
+
1639
+ if (directory !== undefined && liveReload) {
1640
+ watch([directory], {
1641
+ persistent: true,
1642
+ ignoreInitial: true,
1643
+ }).on("all", async () => {
1644
+ await miniflare.reload();
1645
+ });
1646
+ }
1647
+
1648
+ CLEANUP_CALLBACKS.push(() => {
1649
+ server.close();
1650
+ miniflare.dispose().catch((err) => miniflare.log.error(err));
1651
+ });
1652
+ } catch (e) {
1653
+ miniflare.log.error(e as Error);
1654
+ CLEANUP();
1655
+ throw new FatalError("Could not start Miniflare.", 1);
1656
+ }
831
1657
  }
1658
+ )
1659
+ .command("functions", false, (yargs) =>
1660
+ // we hide this command from help output because
1661
+ // it's not meant to be used directly right now
1662
+ yargs.command(
1663
+ "build [directory]",
1664
+ "Compile a folder of Cloudflare Pages Functions into a single Worker",
1665
+ (yargs) =>
1666
+ yargs
1667
+ .positional("directory", {
1668
+ type: "string",
1669
+ default: "functions",
1670
+ description: "The directory of Pages Functions",
1671
+ })
1672
+ .options({
1673
+ outfile: {
1674
+ type: "string",
1675
+ default: "_worker.js",
1676
+ description: "The location of the output Worker script",
1677
+ },
1678
+ "output-config-path": {
1679
+ type: "string",
1680
+ description: "The location for the output config file",
1681
+ },
1682
+ minify: {
1683
+ type: "boolean",
1684
+ default: false,
1685
+ description: "Minify the output Worker script",
1686
+ },
1687
+ sourcemap: {
1688
+ type: "boolean",
1689
+ default: false,
1690
+ description:
1691
+ "Generate a sourcemap for the output Worker script",
1692
+ },
1693
+ "fallback-service": {
1694
+ type: "string",
1695
+ default: "ASSETS",
1696
+ description:
1697
+ "The service to fallback to at the end of the `next` chain. Setting to '' will fallback to the global `fetch`.",
1698
+ },
1699
+ watch: {
1700
+ type: "boolean",
1701
+ default: false,
1702
+ description:
1703
+ "Watch for changes to the functions and automatically rebuild the Worker script",
1704
+ },
1705
+ plugin: {
1706
+ type: "boolean",
1707
+ default: false,
1708
+ description: "Build a plugin rather than a Worker script",
1709
+ },
1710
+ "build-output-directory": {
1711
+ type: "string",
1712
+ description: "The directory to output static assets to",
1713
+ },
1714
+ "node-compat": {
1715
+ describe: "Enable node.js compatibility",
1716
+ default: false,
1717
+ type: "boolean",
1718
+ hidden: true,
1719
+ },
1720
+ })
1721
+ .epilogue(pagesBetaWarning),
1722
+ async ({
1723
+ directory,
1724
+ outfile,
1725
+ "output-config-path": outputConfigPath,
1726
+ minify,
1727
+ sourcemap,
1728
+ fallbackService,
1729
+ watch,
1730
+ plugin,
1731
+ "build-output-directory": buildOutputDirectory,
1732
+ "node-compat": nodeCompat,
1733
+ }) => {
1734
+ if (!isInPagesCI) {
1735
+ // Beta message for `wrangler pages <commands>` usage
1736
+ logger.log(pagesBetaWarning);
1737
+ }
832
1738
 
833
- process.on("SIGINT", () => {
834
- server.close();
835
- miniflare.dispose();
836
- });
837
- process.on("SIGTERM", () => {
838
- server.close();
839
- miniflare.dispose();
840
- });
841
- }
1739
+ if (nodeCompat) {
1740
+ console.warn(
1741
+ "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."
1742
+ );
1743
+ }
1744
+
1745
+ buildOutputDirectory ??= dirname(outfile);
1746
+
1747
+ await buildFunctions({
1748
+ outfile,
1749
+ outputConfigPath,
1750
+ functionsDirectory: directory,
1751
+ minify,
1752
+ sourcemap,
1753
+ fallbackService,
1754
+ watch,
1755
+ plugin,
1756
+ buildOutputDirectory,
1757
+ nodeCompat,
1758
+ });
1759
+ }
1760
+ )
1761
+ )
1762
+ .command("project", "⚡️ Interact with your Pages projects", (yargs) =>
1763
+ yargs
1764
+ .command(
1765
+ "list",
1766
+ "List your Cloudflare Pages projects",
1767
+ (yargs) => yargs.epilogue(pagesBetaWarning),
1768
+ async () => {
1769
+ const config = getConfigCache<PagesConfigCache>(
1770
+ PAGES_CONFIG_CACHE_FILENAME
1771
+ );
1772
+
1773
+ const accountId = await requireAuth(config);
1774
+
1775
+ const projects: Array<Project> = await listProjects({ accountId });
1776
+
1777
+ const data = projects.map((project) => {
1778
+ return {
1779
+ "Project Name": project.name,
1780
+ "Project Domains": `${project.domains.join(", ")}`,
1781
+ "Git Provider": project.source ? "Yes" : "No",
1782
+ "Last Modified": project.latest_deployment
1783
+ ? timeagoFormat(project.latest_deployment.modified_on)
1784
+ : timeagoFormat(project.created_on),
1785
+ };
1786
+ });
1787
+
1788
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1789
+ account_id: accountId,
1790
+ });
1791
+
1792
+ render(<Table data={data}></Table>);
1793
+ }
1794
+ )
1795
+ .command(
1796
+ "create [project-name]",
1797
+ "Create a new Cloudflare Pages project",
1798
+ (yargs) =>
1799
+ yargs
1800
+ .positional("project-name", {
1801
+ type: "string",
1802
+ demandOption: true,
1803
+ description: "The name of your Pages project",
1804
+ })
1805
+ .options({
1806
+ "production-branch": {
1807
+ type: "string",
1808
+ description:
1809
+ "The name of the production branch of your project",
1810
+ },
1811
+ })
1812
+ .epilogue(pagesBetaWarning),
1813
+ async ({ productionBranch, projectName }) => {
1814
+ const config = getConfigCache<PagesConfigCache>(
1815
+ PAGES_CONFIG_CACHE_FILENAME
1816
+ );
1817
+ const accountId = await requireAuth(config);
1818
+
1819
+ const isInteractive = process.stdin.isTTY;
1820
+ if (!projectName && isInteractive) {
1821
+ projectName = await prompt("Enter the name of your new project:");
1822
+ }
1823
+
1824
+ if (!projectName) {
1825
+ throw new FatalError("Must specify a project name.", 1);
1826
+ }
1827
+
1828
+ if (!productionBranch && isInteractive) {
1829
+ let isGitDir = true;
1830
+ try {
1831
+ execSync(`git rev-parse --is-inside-work-tree`, {
1832
+ stdio: "ignore",
1833
+ });
1834
+ } catch (err) {
1835
+ isGitDir = false;
1836
+ }
1837
+
1838
+ productionBranch = await prompt(
1839
+ "Enter the production branch name:",
1840
+ "text",
1841
+ isGitDir
1842
+ ? execSync(`git rev-parse --abbrev-ref HEAD`)
1843
+ .toString()
1844
+ .trim()
1845
+ : "production"
1846
+ );
1847
+ }
1848
+
1849
+ if (!productionBranch) {
1850
+ throw new FatalError("Must specify a production branch.", 1);
1851
+ }
1852
+
1853
+ const { subdomain } = await fetchResult<Project>(
1854
+ `/accounts/${accountId}/pages/projects`,
1855
+ {
1856
+ method: "POST",
1857
+ body: JSON.stringify({
1858
+ name: projectName,
1859
+ production_branch: productionBranch,
1860
+ }),
1861
+ }
1862
+ );
1863
+
1864
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1865
+ account_id: accountId,
1866
+ project_name: projectName,
1867
+ });
1868
+
1869
+ logger.log(
1870
+ `✨ Successfully created the '${projectName}' project. It will be available at https://${subdomain}/ once you create your first deployment.`
1871
+ );
1872
+ logger.log(
1873
+ `To deploy a folder of assets, run 'wrangler pages publish [directory]'.`
1874
+ );
1875
+ }
1876
+ )
1877
+ .epilogue(pagesBetaWarning)
1878
+ )
1879
+ .command(
1880
+ "deployment",
1881
+ "🚀 Interact with the deployments of a project",
1882
+ (yargs) =>
1883
+ yargs
1884
+ .command(
1885
+ "list",
1886
+ "List deployments in your Cloudflare Pages project",
1887
+ (yargs) =>
1888
+ yargs
1889
+ .options({
1890
+ "project-name": {
1891
+ type: "string",
1892
+ description:
1893
+ "The name of the project you would like to list deployments for",
1894
+ },
1895
+ })
1896
+ .epilogue(pagesBetaWarning),
1897
+ async ({ projectName }) => {
1898
+ const config = getConfigCache<PagesConfigCache>(
1899
+ PAGES_CONFIG_CACHE_FILENAME
1900
+ );
1901
+ const accountId = await requireAuth(config);
1902
+
1903
+ projectName ??= config.project_name;
1904
+
1905
+ const isInteractive = process.stdin.isTTY;
1906
+ if (!projectName && isInteractive) {
1907
+ const projects = await listProjects({ accountId });
1908
+ projectName = await new Promise((resolve) => {
1909
+ const { unmount } = render(
1910
+ <>
1911
+ <Text>Select a project:</Text>
1912
+ <SelectInput
1913
+ items={projects.map((project) => ({
1914
+ key: project.name,
1915
+ label: project.name,
1916
+ value: project,
1917
+ }))}
1918
+ onSelect={async (selected) => {
1919
+ resolve(selected.value.name);
1920
+ unmount();
1921
+ }}
1922
+ />
1923
+ </>
1924
+ );
1925
+ });
1926
+ }
1927
+
1928
+ if (!projectName) {
1929
+ throw new FatalError("Must specify a project name.", 1);
1930
+ }
1931
+
1932
+ const deployments: Array<Deployment> = await fetchResult(
1933
+ `/accounts/${accountId}/pages/projects/${projectName}/deployments`
1934
+ );
1935
+
1936
+ const titleCase = (word: string) =>
1937
+ word.charAt(0).toUpperCase() + word.slice(1);
1938
+
1939
+ const shortSha = (sha: string) => sha.slice(0, 7);
1940
+
1941
+ const getStatus = (deployment: Deployment) => {
1942
+ // Return a pretty time since timestamp if successful otherwise the status
1943
+ if (deployment.latest_stage.status === `success`) {
1944
+ return timeagoFormat(deployment.latest_stage.ended_on);
1945
+ }
1946
+ return titleCase(deployment.latest_stage.status);
1947
+ };
1948
+
1949
+ const data = deployments.map((deployment) => {
1950
+ return {
1951
+ Environment: titleCase(deployment.environment),
1952
+ Branch: deployment.deployment_trigger.metadata.branch,
1953
+ Source: shortSha(
1954
+ deployment.deployment_trigger.metadata.commit_hash
1955
+ ),
1956
+ Deployment: deployment.url,
1957
+ Status: getStatus(deployment),
1958
+ // TODO: Use a url shortener
1959
+ Build: `https://dash.cloudflare.com/${accountId}/pages/view/${deployment.project_name}/${deployment.id}`,
1960
+ };
1961
+ });
1962
+
1963
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1964
+ account_id: accountId,
1965
+ });
1966
+
1967
+ render(<Table data={data}></Table>);
1968
+ }
1969
+ )
1970
+ .command({
1971
+ command: "create [directory]",
1972
+ ...createDeployment,
1973
+ } as CommandModule)
1974
+ .epilogue(pagesBetaWarning)
1975
+ )
1976
+ .command({
1977
+ command: "publish [directory]",
1978
+ ...createDeployment,
1979
+ } as CommandModule);
1980
+ };
1981
+
1982
+ const invalidAssetsFetch: typeof miniflareFetch = () => {
1983
+ throw new Error(
1984
+ "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
842
1985
  );
843
1986
  };
1987
+
1988
+ const listProjects = async ({
1989
+ accountId,
1990
+ }: {
1991
+ accountId: string;
1992
+ }): Promise<Array<Project>> => {
1993
+ const pageSize = 10;
1994
+ let page = 1;
1995
+ const results = [];
1996
+ while (results.length % pageSize === 0) {
1997
+ const json: Array<Project> = await fetchResult(
1998
+ `/accounts/${accountId}/pages/projects`,
1999
+ {},
2000
+ new URLSearchParams({
2001
+ per_page: pageSize.toString(),
2002
+ page: page.toString(),
2003
+ })
2004
+ );
2005
+ page++;
2006
+ results.push(...json);
2007
+ if (json.length < pageSize) {
2008
+ break;
2009
+ }
2010
+ }
2011
+ return results;
2012
+ };
2013
+
2014
+ function formatTime(duration: number) {
2015
+ return `(${(duration / 1000).toFixed(2)} sec)`;
2016
+ }
2017
+
2018
+ function Progress({ done, total }: { done: number; total: number }) {
2019
+ return (
2020
+ <>
2021
+ <Text>
2022
+ <Spinner type="earth" />
2023
+ {` Uploading... (${done}/${total})\n`}
2024
+ </Text>
2025
+ </>
2026
+ );
2027
+ }