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.
- package/Cloudflare_CA.pem +18 -0
- package/README.md +27 -14
- package/bin/wrangler.js +48 -4
- package/miniflare-dist/index.mjs +4886 -0
- package/package.json +60 -31
- package/pages/functions/buildPlugin.ts +105 -0
- package/pages/functions/buildWorker.ts +93 -4
- package/pages/functions/filepath-routing.test.ts +223 -28
- package/pages/functions/filepath-routing.ts +100 -132
- package/pages/functions/routes.ts +16 -18
- package/pages/functions/template-plugin.ts +147 -0
- package/pages/functions/template-worker.ts +37 -38
- package/src/__tests__/config-cache.test.ts +36 -0
- package/src/__tests__/configuration.test.ts +3487 -0
- package/src/__tests__/dev.test.tsx +977 -61
- package/src/__tests__/guess-worker-format.test.ts +85 -0
- package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
- package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
- package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
- package/src/__tests__/helpers/mock-account-id.ts +48 -0
- package/src/__tests__/helpers/mock-bin.ts +36 -0
- package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +56 -16
- package/src/__tests__/helpers/mock-console.ts +87 -0
- package/src/__tests__/helpers/mock-dialogs.ts +130 -0
- package/src/__tests__/helpers/mock-http-server.ts +46 -0
- package/src/__tests__/helpers/mock-istty.ts +27 -0
- package/src/__tests__/helpers/mock-kv.ts +33 -0
- package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
- package/src/__tests__/helpers/mock-stdin.ts +103 -0
- package/src/__tests__/helpers/mock-web-socket.ts +29 -0
- package/src/__tests__/helpers/run-in-tmp.ts +39 -0
- package/src/__tests__/helpers/run-wrangler.ts +16 -0
- package/src/__tests__/helpers/write-worker-source.ts +31 -0
- package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
- package/src/__tests__/https-options.test.ts +130 -0
- package/src/__tests__/index.test.ts +179 -209
- package/src/__tests__/init.test.ts +1637 -0
- package/src/__tests__/jest.setup.ts +61 -6
- package/src/__tests__/kv.test.ts +842 -472
- package/src/__tests__/logger.test.ts +115 -0
- package/src/__tests__/package-manager.test.ts +353 -0
- package/src/__tests__/pages.test.ts +754 -0
- package/src/__tests__/parse.test.ts +330 -0
- package/src/__tests__/publish.test.ts +5755 -0
- package/src/__tests__/r2.test.ts +233 -0
- package/src/__tests__/route.test.ts +45 -0
- package/src/__tests__/secret.test.ts +481 -0
- package/src/__tests__/tail.test.ts +690 -0
- package/src/__tests__/user.test.ts +131 -0
- package/src/__tests__/whoami.test.tsx +139 -0
- package/src/abort.d.ts +3 -0
- package/src/bundle.ts +197 -0
- package/src/cfetch/index.ts +40 -21
- package/src/cfetch/internal.ts +102 -29
- package/src/cli.ts +6 -2
- package/src/config/README.md +107 -0
- package/src/config/config.ts +234 -0
- package/src/config/diagnostics.ts +80 -0
- package/src/config/environment.ts +420 -0
- package/src/config/index.ts +225 -0
- package/src/config/validation-helpers.ts +584 -0
- package/src/config/validation.ts +1548 -0
- package/src/config-cache.ts +45 -0
- package/src/create-worker-preview.ts +185 -0
- package/src/create-worker-upload-form.ts +254 -0
- package/src/dev/dev-vars.ts +36 -0
- package/src/dev/dev.tsx +401 -0
- package/src/dev/local.tsx +326 -0
- package/src/dev/remote.tsx +237 -0
- package/src/dev/use-esbuild.ts +118 -0
- package/src/dialogs.tsx +66 -8
- package/src/durable.ts +102 -0
- package/src/entry.ts +322 -0
- package/src/environment-variables.ts +35 -0
- package/src/errors.ts +11 -0
- package/src/https-options.ts +123 -0
- package/src/index.tsx +1848 -976
- package/src/inspect.ts +156 -33
- package/src/intl-polyfill.d.ts +139 -0
- package/src/{kv.tsx → kv.ts} +199 -67
- package/src/logger.ts +66 -0
- package/src/miniflare-cli/README.md +30 -0
- package/src/miniflare-cli/enum-keys.ts +17 -0
- package/src/miniflare-cli/index.ts +41 -0
- package/src/miniflare-cli/request-context.ts +40 -0
- package/src/module-collection.ts +229 -35
- package/src/open-in-browser.ts +19 -0
- package/src/package-manager.ts +174 -0
- package/src/pages.tsx +1162 -100
- package/src/parse.ts +203 -0
- package/src/paths.ts +26 -0
- package/src/proxy.ts +227 -51
- package/src/publish.ts +633 -276
- package/src/r2.ts +50 -0
- package/src/selfsigned.d.ts +29 -0
- package/src/sites.tsx +270 -62
- package/src/tail/filters.ts +279 -0
- package/src/tail/index.ts +299 -0
- package/src/tail/printing.ts +67 -0
- package/src/update-check.ts +19 -0
- package/src/user.tsx +382 -195
- package/src/whoami.tsx +71 -0
- package/src/{api/worker.ts → worker.ts} +66 -40
- package/src/zones.ts +73 -0
- package/templates/gitignore +171 -0
- package/templates/new-worker-scheduled.js +17 -0
- package/templates/new-worker-scheduled.ts +32 -0
- package/templates/new-worker.js +15 -0
- package/templates/new-worker.ts +30 -0
- package/{static-asset-facade.js → templates/static-asset-facade.js} +1 -5
- package/templates/tsconfig.json +105 -0
- package/wrangler-dist/cli.js +106332 -103741
- package/src/__tests__/mock-dialogs.ts +0 -65
- package/src/__tests__/run-in-tmp.ts +0 -19
- package/src/__tests__/run-wrangler.ts +0 -32
- package/src/api/form_data.ts +0 -131
- package/src/api/preview.ts +0 -128
- package/src/config.ts +0 -487
- package/src/dev.tsx +0 -784
- package/src/tail.tsx +0 -73
- package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
- package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
- package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
- package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
- package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
- package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
- package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
- package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
- package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
- package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
- package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
- package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
- package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
- package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
- package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
- package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
- package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
- package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
- package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
- package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
- package/vendor/wrangler-mime/CHANGELOG.md +0 -289
- package/vendor/wrangler-mime/LICENSE +0 -21
- package/vendor/wrangler-mime/Mime.js +0 -97
- package/vendor/wrangler-mime/README.md +0 -187
- package/vendor/wrangler-mime/cli.js +0 -46
- package/vendor/wrangler-mime/index.js +0 -4
- package/vendor/wrangler-mime/lite.js +0 -4
- package/vendor/wrangler-mime/package.json +0 -52
- package/vendor/wrangler-mime/types/other.js +0 -1
- package/vendor/wrangler-mime/types/standard.js +0 -1
- 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
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { tmpdir } from "os";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { URL } from "url";
|
|
10
|
-
import {
|
|
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
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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", () =>
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
+
CLEANUP_CALLBACKS.push(() => {
|
|
210
|
+
proxy.kill();
|
|
211
|
+
});
|
|
126
212
|
|
|
127
213
|
proxy.stdout.on("data", (data) => {
|
|
128
|
-
|
|
214
|
+
logger.log(`[proxy]: ${data}`);
|
|
129
215
|
});
|
|
130
216
|
|
|
131
217
|
proxy.stderr.on("data", (data) => {
|
|
132
|
-
|
|
218
|
+
logger.error(`[proxy]: ${data}`);
|
|
133
219
|
});
|
|
134
220
|
|
|
135
221
|
proxy.on("close", (code) => {
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
515
|
+
logger.log("_headers modified. Re-evaluating...");
|
|
427
516
|
headersMatcher = generateHeadersMatcher(headersFile);
|
|
428
517
|
break;
|
|
429
518
|
}
|
|
430
519
|
case redirectsFile: {
|
|
431
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
600
|
-
deconstructedResponse: {
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
|
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
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
1533
|
+
const outfile = join(
|
|
1534
|
+
tmpdir(),
|
|
1535
|
+
`./functionsWorker-${Math.random()}.js`
|
|
1536
|
+
);
|
|
802
1537
|
|
|
803
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
866
|
-
//
|
|
867
|
-
const assetsFetch =
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
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 = `
|
|
1660
|
+
url.host = `localhost:${proxyPort}`;
|
|
901
1661
|
return await fetch(url, request);
|
|
902
1662
|
} catch (thrown) {
|
|
903
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1699
|
+
logger.log(`Serving at http://localhost:${port}/`);
|
|
939
1700
|
|
|
940
1701
|
if (process.env.BROWSER !== "none") {
|
|
941
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|