wrangler 0.0.0-e22f9d7 → 0.0.0-e273e09

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 (154) 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 +60 -31
  6. package/pages/functions/buildPlugin.ts +105 -0
  7. package/pages/functions/buildWorker.ts +93 -4
  8. package/pages/functions/filepath-routing.test.ts +223 -28
  9. package/pages/functions/filepath-routing.ts +100 -132
  10. package/pages/functions/routes.ts +16 -18
  11. package/pages/functions/template-plugin.ts +147 -0
  12. package/pages/functions/template-worker.ts +37 -38
  13. package/src/__tests__/config-cache.test.ts +36 -0
  14. package/src/__tests__/configuration.test.ts +3487 -0
  15. package/src/__tests__/dev.test.tsx +977 -61
  16. package/src/__tests__/guess-worker-format.test.ts +85 -0
  17. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  18. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  19. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  20. package/src/__tests__/helpers/mock-account-id.ts +48 -0
  21. package/src/__tests__/helpers/mock-bin.ts +36 -0
  22. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +56 -16
  23. package/src/__tests__/helpers/mock-console.ts +87 -0
  24. package/src/__tests__/helpers/mock-dialogs.ts +130 -0
  25. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  26. package/src/__tests__/helpers/mock-istty.ts +27 -0
  27. package/src/__tests__/helpers/mock-kv.ts +33 -0
  28. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  29. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  30. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  31. package/src/__tests__/helpers/run-in-tmp.ts +39 -0
  32. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  33. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  34. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  35. package/src/__tests__/https-options.test.ts +130 -0
  36. package/src/__tests__/index.test.ts +179 -209
  37. package/src/__tests__/init.test.ts +1637 -0
  38. package/src/__tests__/jest.setup.ts +61 -6
  39. package/src/__tests__/kv.test.ts +842 -472
  40. package/src/__tests__/logger.test.ts +115 -0
  41. package/src/__tests__/package-manager.test.ts +353 -0
  42. package/src/__tests__/pages.test.ts +754 -0
  43. package/src/__tests__/parse.test.ts +330 -0
  44. package/src/__tests__/publish.test.ts +5755 -0
  45. package/src/__tests__/r2.test.ts +233 -0
  46. package/src/__tests__/route.test.ts +45 -0
  47. package/src/__tests__/secret.test.ts +481 -0
  48. package/src/__tests__/tail.test.ts +690 -0
  49. package/src/__tests__/user.test.ts +131 -0
  50. package/src/__tests__/whoami.test.tsx +139 -0
  51. package/src/abort.d.ts +3 -0
  52. package/src/bundle.ts +197 -0
  53. package/src/cfetch/index.ts +40 -21
  54. package/src/cfetch/internal.ts +102 -29
  55. package/src/cli.ts +6 -2
  56. package/src/config/README.md +107 -0
  57. package/src/config/config.ts +234 -0
  58. package/src/config/diagnostics.ts +80 -0
  59. package/src/config/environment.ts +420 -0
  60. package/src/config/index.ts +225 -0
  61. package/src/config/validation-helpers.ts +584 -0
  62. package/src/config/validation.ts +1548 -0
  63. package/src/config-cache.ts +45 -0
  64. package/src/create-worker-preview.ts +185 -0
  65. package/src/create-worker-upload-form.ts +254 -0
  66. package/src/dev/dev-vars.ts +36 -0
  67. package/src/dev/dev.tsx +401 -0
  68. package/src/dev/local.tsx +326 -0
  69. package/src/dev/remote.tsx +237 -0
  70. package/src/dev/use-esbuild.ts +118 -0
  71. package/src/dialogs.tsx +66 -8
  72. package/src/durable.ts +102 -0
  73. package/src/entry.ts +322 -0
  74. package/src/environment-variables.ts +35 -0
  75. package/src/errors.ts +11 -0
  76. package/src/https-options.ts +123 -0
  77. package/src/index.tsx +1848 -976
  78. package/src/inspect.ts +156 -33
  79. package/src/intl-polyfill.d.ts +139 -0
  80. package/src/{kv.tsx → kv.ts} +199 -67
  81. package/src/logger.ts +66 -0
  82. package/src/miniflare-cli/README.md +30 -0
  83. package/src/miniflare-cli/enum-keys.ts +17 -0
  84. package/src/miniflare-cli/index.ts +41 -0
  85. package/src/miniflare-cli/request-context.ts +40 -0
  86. package/src/module-collection.ts +229 -35
  87. package/src/open-in-browser.ts +19 -0
  88. package/src/package-manager.ts +174 -0
  89. package/src/pages.tsx +1162 -100
  90. package/src/parse.ts +203 -0
  91. package/src/paths.ts +26 -0
  92. package/src/proxy.ts +227 -51
  93. package/src/publish.ts +633 -276
  94. package/src/r2.ts +50 -0
  95. package/src/selfsigned.d.ts +29 -0
  96. package/src/sites.tsx +270 -62
  97. package/src/tail/filters.ts +279 -0
  98. package/src/tail/index.ts +299 -0
  99. package/src/tail/printing.ts +67 -0
  100. package/src/update-check.ts +19 -0
  101. package/src/user.tsx +382 -195
  102. package/src/whoami.tsx +71 -0
  103. package/src/{api/worker.ts → worker.ts} +66 -40
  104. package/src/zones.ts +73 -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 -5
  111. package/templates/tsconfig.json +105 -0
  112. package/wrangler-dist/cli.js +106332 -103741
  113. package/src/__tests__/mock-dialogs.ts +0 -65
  114. package/src/__tests__/run-in-tmp.ts +0 -19
  115. package/src/__tests__/run-wrangler.ts +0 -32
  116. package/src/api/form_data.ts +0 -131
  117. package/src/api/preview.ts +0 -128
  118. package/src/config.ts +0 -487
  119. package/src/dev.tsx +0 -784
  120. package/src/tail.tsx +0 -73
  121. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  122. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  123. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  124. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  125. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  126. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  127. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  128. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  129. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  130. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  131. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  132. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  133. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  134. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  135. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  136. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  137. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  138. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  139. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  140. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  141. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  142. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  143. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  144. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  145. package/vendor/wrangler-mime/LICENSE +0 -21
  146. package/vendor/wrangler-mime/Mime.js +0 -97
  147. package/vendor/wrangler-mime/README.md +0 -187
  148. package/vendor/wrangler-mime/cli.js +0 -46
  149. package/vendor/wrangler-mime/index.js +0 -4
  150. package/vendor/wrangler-mime/lite.js +0 -4
  151. package/vendor/wrangler-mime/package.json +0 -52
  152. package/vendor/wrangler-mime/types/other.js +0 -1
  153. package/vendor/wrangler-mime/types/standard.js +0 -1
  154. package/wrangler-dist/cli.js.map +0 -7
package/src/pages.tsx CHANGED
@@ -1,37 +1,119 @@
1
1
  /* eslint-disable no-shadow */
2
2
 
3
- import assert from "assert";
4
- import type { BuilderCallback } from "yargs";
5
- import { join } from "path";
6
- import { tmpdir } from "os";
7
- import { existsSync, lstatSync, readFileSync, writeFileSync } from "fs";
8
- import { execSync, spawn } from "child_process";
9
- import { URL } from "url";
10
- import { getType } from "mime";
11
- 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";
12
11
  import { watch } from "chokidar";
13
- 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";
14
23
  import { buildWorker } from "../pages/functions/buildWorker";
15
- import type { Config } from "../pages/functions/routes";
16
- import { writeRoutesModule } from "../pages/functions/routes";
17
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;
18
93
 
19
94
  // Defer importing miniflare until we really need it. This takes ~0.5s
20
95
  // and also modifies some `stream/web` and `undici` prototypes, so we
21
96
  // don't want to do this if pages commands aren't being called.
22
- import type { Headers, Request, fetch } from "@miniflare/core";
23
- import type { MiniflareOptions } from "miniflare";
24
97
 
25
- const EXIT_CALLBACKS = [];
26
- const EXIT = (message?: string, code?: number) => {
27
- if (message) console.log(message);
28
- if (code) process.exitCode = code;
29
- EXIT_CALLBACKS.forEach((callback) => callback());
30
- process.exit(code);
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?.());
31
107
  };
32
108
 
33
- process.on("SIGINT", () => EXIT());
34
- process.on("SIGTERM", () => EXIT());
109
+ process.on("SIGINT", () => {
110
+ CLEANUP();
111
+ process.exit();
112
+ });
113
+ process.on("SIGTERM", () => {
114
+ CLEANUP();
115
+ process.exit();
116
+ });
35
117
 
36
118
  function isWindows() {
37
119
  return process.platform === "win32";
@@ -91,7 +173,7 @@ function getPort(pid: number) {
91
173
  const match = matches[0];
92
174
  if (match) return parseInt(match[1]);
93
175
  } catch (thrown) {
94
- console.error(
176
+ logger.error(
95
177
  `Error scanning for ports of process with PID ${pid}: ${thrown}`
96
178
  );
97
179
  }
@@ -104,13 +186,15 @@ async function spawnProxyProcess({
104
186
  port?: number;
105
187
  command: (string | number)[];
106
188
  }): Promise<void | number> {
107
- if (command.length === 0)
108
- return EXIT(
189
+ if (command.length === 0) {
190
+ CLEANUP();
191
+ throw new FatalError(
109
192
  "Must specify a directory of static assets to serve or a command to run.",
110
193
  1
111
194
  );
195
+ }
112
196
 
113
- console.log(`Running ${command.join(" ")}...`);
197
+ logger.log(`Running ${command.join(" ")}...`);
114
198
  const proxy = spawn(
115
199
  command[0].toString(),
116
200
  command.slice(1).map((value) => value.toString()),
@@ -122,28 +206,30 @@ async function spawnProxyProcess({
122
206
  },
123
207
  }
124
208
  );
125
- EXIT_CALLBACKS.push(() => proxy.kill());
209
+ CLEANUP_CALLBACKS.push(() => {
210
+ proxy.kill();
211
+ });
126
212
 
127
213
  proxy.stdout.on("data", (data) => {
128
- console.log(`[proxy]: ${data}`);
214
+ logger.log(`[proxy]: ${data}`);
129
215
  });
130
216
 
131
217
  proxy.stderr.on("data", (data) => {
132
- console.error(`[proxy]: ${data}`);
218
+ logger.error(`[proxy]: ${data}`);
133
219
  });
134
220
 
135
221
  proxy.on("close", (code) => {
136
- console.error(`Proxy exited with status ${code}.`);
222
+ logger.error(`Proxy exited with status ${code}.`);
137
223
  });
138
224
 
139
225
  // Wait for proxy process to start...
140
226
  while (!proxy.pid) {}
141
227
 
142
228
  if (port === undefined) {
143
- console.log(
229
+ logger.log(
144
230
  `Sleeping ${SECONDS_TO_WAIT_FOR_PROXY} seconds to allow proxy process to start before attempting to automatically determine port...`
145
231
  );
146
- console.log("To skip, specify the proxy port with --proxy.");
232
+ logger.log("To skip, specify the proxy port with --proxy.");
147
233
  await sleep(SECONDS_TO_WAIT_FOR_PROXY * 1000);
148
234
 
149
235
  port = getPids(proxy.pid)
@@ -151,12 +237,13 @@ async function spawnProxyProcess({
151
237
  .filter((port) => port !== undefined)[0];
152
238
 
153
239
  if (port === undefined) {
154
- return EXIT(
240
+ CLEANUP();
241
+ throw new FatalError(
155
242
  "Could not automatically determine proxy port. Please specify the proxy port with --proxy.",
156
243
  1
157
244
  );
158
245
  } else {
159
- console.log(`Automatically determined the proxy port to be ${port}.`);
246
+ logger.log(`Automatically determined the proxy port to be ${port}.`);
160
247
  }
161
248
  }
162
249
 
@@ -213,7 +300,7 @@ function generateRulesMatcher<T>(
213
300
  T
214
301
  ][];
215
302
 
216
- return ({ request }: { request: Request }) => {
303
+ return ({ request }: { request: MiniflareRequest }) => {
217
304
  const { pathname, host } = new URL(request.url);
218
305
 
219
306
  return compiledRules
@@ -286,7 +373,7 @@ function generateHeadersMatcher(headersFile: string) {
286
373
  )
287
374
  );
288
375
 
289
- return (request: Request) => {
376
+ return (request: MiniflareRequest) => {
290
377
  const matches = rulesMatcher({
291
378
  request,
292
379
  });
@@ -335,7 +422,7 @@ function generateRedirectsMatcher(redirectsFile: string) {
335
422
  })
336
423
  );
337
424
 
338
- return (request: Request) => {
425
+ return (request: MiniflareRequest) => {
339
426
  const match = rulesMatcher({
340
427
  request,
341
428
  })[0];
@@ -390,7 +477,9 @@ function hasFileExtension(pathname: string) {
390
477
  return /\/.+\.[a-z0-9]+$/i.test(pathname);
391
478
  }
392
479
 
393
- async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
480
+ async function generateAssetsFetch(
481
+ directory: string
482
+ ): Promise<typeof miniflareFetch> {
394
483
  // Defer importing miniflare until we really need it
395
484
  const { Headers, Request, Response } = await import("@miniflare/core");
396
485
 
@@ -423,12 +512,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
423
512
  }).on("change", (path) => {
424
513
  switch (path) {
425
514
  case headersFile: {
426
- console.log("_headers modified. Re-evaluating...");
515
+ logger.log("_headers modified. Re-evaluating...");
427
516
  headersMatcher = generateHeadersMatcher(headersFile);
428
517
  break;
429
518
  }
430
519
  case redirectsFile: {
431
- console.log("_redirects modified. Re-evaluating...");
520
+ logger.log("_redirects modified. Re-evaluating...");
432
521
  redirectsMatcher = generateRedirectsMatcher(redirectsFile);
433
522
  break;
434
523
  }
@@ -439,12 +528,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
439
528
  return readFileSync(file);
440
529
  };
441
530
 
442
- const generateResponse = (request: Request) => {
531
+ const generateResponse = (request: MiniflareRequest) => {
443
532
  const url = new URL(request.url);
444
533
 
445
534
  const deconstructedResponse: {
446
535
  status: number;
447
- headers: Headers;
536
+ headers: MiniflareHeaders;
448
537
  body?: Buffer;
449
538
  } = {
450
539
  status: 200,
@@ -596,8 +685,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
596
685
  };
597
686
 
598
687
  const attachHeaders = (
599
- request: Request,
600
- deconstructedResponse: { status: number; headers: Headers; body?: Buffer }
688
+ request: MiniflareRequest,
689
+ deconstructedResponse: {
690
+ status: number;
691
+ headers: MiniflareHeaders;
692
+ body?: Buffer;
693
+ }
601
694
  ) => {
602
695
  const headers = deconstructedResponse.headers;
603
696
  const newHeaders = new Headers({});
@@ -641,7 +734,7 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
641
734
  const RUNNING_BUILDERS: BuildResult[] = [];
642
735
 
643
736
  async function buildFunctions({
644
- scriptPath,
737
+ outfile,
645
738
  outputConfigPath,
646
739
  functionsDirectory,
647
740
  minify = false,
@@ -649,8 +742,11 @@ async function buildFunctions({
649
742
  fallbackService = "ASSETS",
650
743
  watch = false,
651
744
  onEnd,
745
+ plugin = false,
746
+ buildOutputDirectory,
747
+ nodeCompat,
652
748
  }: {
653
- scriptPath: string;
749
+ outfile: string;
654
750
  outputConfigPath?: string;
655
751
  functionsDirectory: string;
656
752
  minify?: boolean;
@@ -658,22 +754,26 @@ async function buildFunctions({
658
754
  fallbackService?: string;
659
755
  watch?: boolean;
660
756
  onEnd?: () => void;
757
+ plugin?: boolean;
758
+ buildOutputDirectory?: string;
759
+ nodeCompat?: boolean;
661
760
  }) {
662
761
  RUNNING_BUILDERS.forEach(
663
762
  (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
664
763
  );
665
764
 
666
- const routesModule = join(tmpdir(), "./functionsRoutes.mjs");
765
+ const routesModule = join(tmpdir(), `./functionsRoutes-${Math.random()}.mjs`);
766
+ const baseURL = toUrlPath("/");
667
767
 
668
768
  const config: Config = await generateConfigFromFileTree({
669
769
  baseDir: functionsDirectory,
670
- baseURL: "/",
770
+ baseURL,
671
771
  });
672
772
 
673
773
  if (outputConfigPath) {
674
774
  writeFileSync(
675
775
  outputConfigPath,
676
- JSON.stringify({ ...config, baseURL: "/" }, null, 2)
776
+ JSON.stringify({ ...config, baseURL }, null, 2)
677
777
  );
678
778
  }
679
779
 
@@ -683,23 +783,644 @@ async function buildFunctions({
683
783
  outfile: routesModule,
684
784
  });
685
785
 
686
- RUNNING_BUILDERS.push(
687
- await buildWorker({
688
- routesModule,
689
- outfile: scriptPath,
690
- minify,
691
- sourcemap,
692
- fallbackService,
693
- watch,
694
- onEnd,
695
- })
696
- );
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
+ }
697
813
  }
698
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 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
+
699
1420
  export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
700
1421
  return yargs
701
1422
  .command(
702
- "dev [directory] [-- command]",
1423
+ "dev [directory] [-- command..]",
703
1424
  "🧑‍💻 Develop your full-stack Pages application locally",
704
1425
  (yargs) => {
705
1426
  return yargs
@@ -755,8 +1476,15 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
755
1476
  default: false,
756
1477
  description: "Auto reload HTML pages when change is detected",
757
1478
  },
1479
+ "node-compat": {
1480
+ describe: "Enable node.js compatibility",
1481
+ default: false,
1482
+ type: "boolean",
1483
+ hidden: true,
1484
+ },
758
1485
  // TODO: Miniflare user options
759
- });
1486
+ })
1487
+ .epilogue(pagesBetaWarning);
760
1488
  },
761
1489
  async ({
762
1490
  local,
@@ -768,10 +1496,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
768
1496
  kv: kvs = [],
769
1497
  do: durableObjects = [],
770
1498
  "live-reload": liveReload,
771
- "--": remaining = [],
1499
+ "node-compat": nodeCompat,
1500
+ _: [_pages, _dev, ...remaining],
772
1501
  }) => {
1502
+ // Beta message for `wrangler pages <commands>` usage
1503
+ logger.log(pagesBetaWarning);
1504
+
773
1505
  if (!local) {
774
- console.error("Only local mode is supported at the moment.");
1506
+ logger.error("Only local mode is supported at the moment.");
775
1507
  return;
776
1508
  }
777
1509
 
@@ -792,41 +1524,59 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
792
1524
 
793
1525
  let miniflareArgs: MiniflareOptions = {};
794
1526
 
795
- let scriptReadyResolve;
796
- const scriptReadyPromise = new Promise(
1527
+ let scriptReadyResolve: () => void;
1528
+ const scriptReadyPromise = new Promise<void>(
797
1529
  (resolve) => (scriptReadyResolve = resolve)
798
1530
  );
799
1531
 
800
1532
  if (usingFunctions) {
801
- const scriptPath = join(tmpdir(), "./functionsWorker.js");
1533
+ const outfile = join(
1534
+ tmpdir(),
1535
+ `./functionsWorker-${Math.random()}.js`
1536
+ );
802
1537
 
803
- 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
+ }
804
1543
 
805
- await buildFunctions({
806
- scriptPath,
807
- functionsDirectory,
808
- sourcemap: true,
809
- watch: true,
810
- onEnd: () => scriptReadyResolve(),
811
- });
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 {}
812
1557
 
813
1558
  watch([functionsDirectory], {
814
1559
  persistent: true,
815
1560
  ignoreInitial: true,
816
1561
  }).on("all", async () => {
817
1562
  await buildFunctions({
818
- scriptPath,
1563
+ outfile,
819
1564
  functionsDirectory,
820
1565
  sourcemap: true,
821
1566
  watch: true,
822
1567
  onEnd: () => scriptReadyResolve(),
1568
+ buildOutputDirectory: directory,
1569
+ nodeCompat,
823
1570
  });
824
1571
  });
825
1572
 
826
1573
  miniflareArgs = {
827
- scriptPath,
1574
+ scriptPath: outfile,
828
1575
  };
829
1576
  } else {
1577
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1578
+ scriptReadyResolve!();
1579
+
830
1580
  const scriptPath =
831
1581
  directory !== undefined
832
1582
  ? join(directory, singleWorkerScriptPath)
@@ -837,8 +1587,11 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
837
1587
  scriptPath,
838
1588
  };
839
1589
  } else {
840
- console.log("No functions. Shimming...");
1590
+ logger.log("No functions. Shimming...");
841
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,
842
1595
  // TODO: The fact that these request/response hacks are necessary is ridiculous.
843
1596
  // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
844
1597
  script: `
@@ -862,11 +1615,16 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
862
1615
  // internally just waits for that promise to resolve.
863
1616
  await scriptReadyPromise;
864
1617
 
865
- // Should only be called if no proxyPort, using `assert.fail()` here
866
- // means the type of `assetsFetch` is still `typeof fetch`
867
- const assetsFetch = proxyPort
868
- ? () => assert.fail()
869
- : await generateAssetsFetch(directory);
1618
+ // `assetsFetch()` will only be called if there is `proxyPort` defined.
1619
+ // We only define `proxyPort`, above, when there is no `directory` defined.
1620
+ const assetsFetch =
1621
+ directory !== undefined
1622
+ ? await generateAssetsFetch(directory)
1623
+ : invalidAssetsFetch;
1624
+
1625
+ const requestContextCheckOptions =
1626
+ await getRequestContextCheckOptions();
1627
+
870
1628
  const miniflare = new Miniflare({
871
1629
  port,
872
1630
  watch: true,
@@ -887,20 +1645,22 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
887
1645
  // User bindings
888
1646
  bindings: {
889
1647
  ...Object.fromEntries(
890
- bindings.map((binding) => binding.toString().split("="))
1648
+ bindings
1649
+ .map((binding) => binding.toString().split("="))
1650
+ .map(([key, ...values]) => [key, values.join("=")])
891
1651
  ),
892
1652
  },
893
1653
 
894
1654
  // env.ASSETS.fetch
895
1655
  serviceBindings: {
896
- async ASSETS(request) {
1656
+ async ASSETS(request: MiniflareRequest) {
897
1657
  if (proxyPort) {
898
1658
  try {
899
1659
  const url = new URL(request.url);
900
- url.host = `127.0.0.1:${proxyPort}`;
1660
+ url.host = `localhost:${proxyPort}`;
901
1661
  return await fetch(url, request);
902
1662
  } catch (thrown) {
903
- console.error(`Could not proxy request: ${thrown}`);
1663
+ logger.error(`Could not proxy request: ${thrown}`);
904
1664
 
905
1665
  // TODO: Pretty error page
906
1666
  return new Response(
@@ -912,7 +1672,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
912
1672
  try {
913
1673
  return await assetsFetch(request);
914
1674
  } catch (thrown) {
915
- console.error(`Could not serve static asset: ${thrown}`);
1675
+ logger.error(`Could not serve static asset: ${thrown}`);
916
1676
 
917
1677
  // TODO: Pretty error page
918
1678
  return new Response(
@@ -929,18 +1689,17 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
929
1689
  cachePersist: true,
930
1690
  liveReload,
931
1691
 
1692
+ ...requestContextCheckOptions,
932
1693
  ...miniflareArgs,
933
1694
  });
934
1695
 
935
1696
  try {
936
1697
  // `startServer` might throw if user code contains errors
937
1698
  const server = await miniflare.startServer();
938
- console.log(`Serving at http://127.0.0.1:${port}/`);
1699
+ logger.log(`Serving at http://localhost:${port}/`);
939
1700
 
940
1701
  if (process.env.BROWSER !== "none") {
941
- const childProcess = await open(`http://127.0.0.1:${port}/`);
942
- // fail silently if the open command doesn't work (e.g. in GitHub Codespaces)
943
- childProcess.on("error", (_err) => {});
1702
+ await openInBrowser(`http://localhost:${port}/`);
944
1703
  }
945
1704
 
946
1705
  if (directory !== undefined && liveReload) {
@@ -952,17 +1711,20 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
952
1711
  });
953
1712
  }
954
1713
 
955
- EXIT_CALLBACKS.push(() => {
1714
+ CLEANUP_CALLBACKS.push(() => {
956
1715
  server.close();
957
1716
  miniflare.dispose().catch((err) => miniflare.log.error(err));
958
1717
  });
959
1718
  } catch (e) {
960
- miniflare.log.error(e);
961
- EXIT("Could not start Miniflare.", 1);
1719
+ miniflare.log.error(e as Error);
1720
+ CLEANUP();
1721
+ throw new FatalError("Could not start Miniflare.", 1);
962
1722
  }
963
1723
  }
964
1724
  )
965
- .command("functions", "Cloudflare Pages Functions", (yargs) =>
1725
+ .command("functions", false, (yargs) =>
1726
+ // we hide this command from help output because
1727
+ // it's not meant to be used directly right now
966
1728
  yargs.command(
967
1729
  "build [directory]",
968
1730
  "Compile a folder of Cloudflare Pages Functions into a single Worker",
@@ -974,7 +1736,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
974
1736
  description: "The directory of Pages Functions",
975
1737
  })
976
1738
  .options({
977
- "script-path": {
1739
+ outfile: {
978
1740
  type: "string",
979
1741
  default: "_worker.js",
980
1742
  description: "The location of the output Worker script",
@@ -1006,26 +1768,326 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1006
1768
  description:
1007
1769
  "Watch for changes to the functions and automatically rebuild the Worker script",
1008
1770
  },
1009
- }),
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),
1010
1788
  async ({
1011
1789
  directory,
1012
- "script-path": scriptPath,
1790
+ outfile,
1013
1791
  "output-config-path": outputConfigPath,
1014
1792
  minify,
1015
1793
  sourcemap,
1016
1794
  fallbackService,
1017
1795
  watch,
1796
+ plugin,
1797
+ "build-output-directory": buildOutputDirectory,
1798
+ "node-compat": nodeCompat,
1018
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
+
1019
1813
  await buildFunctions({
1020
- scriptPath,
1814
+ outfile,
1021
1815
  outputConfigPath,
1022
1816
  functionsDirectory: directory,
1023
1817
  minify,
1024
1818
  sourcemap,
1025
1819
  fallbackService,
1026
1820
  watch,
1821
+ plugin,
1822
+ buildOutputDirectory,
1823
+ nodeCompat,
1027
1824
  });
1028
1825
  }
1029
1826
  )
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);
2046
+ };
2047
+
2048
+ const invalidAssetsFetch: typeof miniflareFetch = () => {
2049
+ throw new Error(
2050
+ "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
2051
+ );
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
+ })
1030
2070
  );
2071
+ page++;
2072
+ results.push(...json);
2073
+ if (json.length < pageSize) {
2074
+ break;
2075
+ }
2076
+ }
2077
+ return results;
1031
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
+ }