wrangler 0.0.0-e151223 → 0.0.0-e1b8ac4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cloudflare_CA.pem +18 -0
- package/README.md +27 -14
- package/bin/wrangler.js +49 -5
- package/kv-asset-handler.js +1 -0
- package/miniflare-dist/index.mjs +4886 -0
- package/package.json +57 -28
- package/pages/functions/buildPlugin.ts +105 -0
- package/pages/functions/buildWorker.ts +93 -4
- package/pages/functions/filepath-routing.test.ts +223 -36
- package/pages/functions/filepath-routing.ts +78 -120
- package/pages/functions/routes.ts +13 -17
- package/pages/functions/template-plugin.ts +147 -0
- package/pages/functions/template-worker.ts +24 -17
- package/src/__tests__/config-cache.test.ts +36 -0
- package/src/__tests__/configuration.test.ts +3548 -0
- package/src/__tests__/dev.test.tsx +1049 -65
- package/src/__tests__/guess-worker-format.test.ts +85 -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 +26 -8
- package/src/__tests__/helpers/mock-bin.ts +36 -0
- package/src/__tests__/helpers/mock-cfetch.ts +48 -10
- package/src/__tests__/helpers/mock-console.ts +39 -8
- package/src/__tests__/helpers/mock-dialogs.ts +99 -11
- 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 +9 -16
- package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
- package/src/__tests__/helpers/mock-process.ts +24 -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 +10 -0
- package/src/__tests__/helpers/run-wrangler.ts +5 -3
- 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 +160 -309
- package/src/__tests__/init.test.ts +1992 -0
- package/src/__tests__/jest.setup.ts +79 -6
- package/src/__tests__/kv.test.ts +651 -280
- package/src/__tests__/logger.test.ts +141 -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 +5671 -959
- package/src/__tests__/r2.test.ts +233 -0
- package/src/__tests__/route.test.ts +45 -0
- package/src/__tests__/secret.test.ts +362 -91
- package/src/__tests__/tail.test.ts +690 -0
- package/src/__tests__/user.test.ts +132 -0
- package/src/__tests__/whoami.test.tsx +60 -7
- package/src/abort.d.ts +3 -0
- package/src/bundle.ts +204 -0
- package/src/cfetch/index.ts +28 -18
- package/src/cfetch/internal.ts +99 -29
- package/src/cli.ts +6 -2
- package/src/config/README.md +107 -0
- package/src/config/config.ts +243 -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 +1580 -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 +399 -0
- package/src/dev/local.tsx +321 -0
- package/src/dev/remote.tsx +242 -0
- package/src/dev/use-esbuild.ts +112 -0
- package/src/dialogs.tsx +64 -6
- 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/generate-auth-url.ts +33 -0
- package/src/generate-random-state.ts +16 -0
- package/src/https-options.ts +123 -0
- package/src/index.tsx +1835 -988
- package/src/inspect.ts +80 -14
- package/src/intl-polyfill.d.ts +139 -0
- package/src/{kv.tsx → kv.ts} +199 -67
- package/src/logger.ts +73 -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 +226 -31
- package/src/open-in-browser.ts +17 -0
- package/src/package-manager.ts +174 -0
- package/src/pages.tsx +1132 -84
- package/src/parse.ts +222 -0
- package/src/proxy.ts +176 -70
- package/src/publish.ts +624 -308
- package/src/r2.ts +50 -0
- package/src/selfsigned.d.ts +29 -0
- package/src/sites.tsx +185 -57
- 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 +349 -166
- package/src/whoami.tsx +5 -3
- package/src/{api/worker.ts → worker.ts} +65 -39
- 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} +3 -7
- package/templates/tsconfig.json +105 -0
- package/wrangler-dist/cli.js +76649 -75066
- package/src/__tests__/helpers/mock-user.ts +0 -27
- package/src/__tests__/logout.test.ts +0 -50
- package/src/api/form_data.ts +0 -134
- package/src/api/preview.ts +0 -128
- package/src/config.ts +0 -641
- package/src/dev.tsx +0 -834
- package/src/npm-installer.ts +0 -20
- 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
|
@@ -2,36 +2,118 @@
|
|
|
2
2
|
|
|
3
3
|
import { execSync, spawn } from "node:child_process";
|
|
4
4
|
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
5
6
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
+
import { dirname, join, sep, extname, basename } from "node:path";
|
|
8
|
+
import { cwd } from "node:process";
|
|
7
9
|
import { URL } from "node:url";
|
|
10
|
+
import { hash } from "blake3-wasm";
|
|
8
11
|
import { watch } from "chokidar";
|
|
12
|
+
import { render, Text } from "ink";
|
|
13
|
+
import SelectInput from "ink-select-input";
|
|
14
|
+
import Spinner from "ink-spinner";
|
|
15
|
+
import Table from "ink-table";
|
|
9
16
|
import { getType } from "mime";
|
|
10
|
-
import
|
|
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";
|
|
11
23
|
import { buildWorker } from "../pages/functions/buildWorker";
|
|
12
24
|
import { generateConfigFromFileTree } from "../pages/functions/filepath-routing";
|
|
13
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";
|
|
14
33
|
import { toUrlPath } from "./paths";
|
|
34
|
+
import { requireAuth } from "./user";
|
|
15
35
|
import type { Config } from "../pages/functions/routes";
|
|
16
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
Headers as MiniflareHeaders,
|
|
38
|
+
Request as MiniflareRequest,
|
|
39
|
+
fetch as miniflareFetch,
|
|
40
|
+
} from "@miniflare/core";
|
|
17
41
|
import type { BuildResult } from "esbuild";
|
|
18
42
|
import type { MiniflareOptions } from "miniflare";
|
|
19
|
-
import type { BuilderCallback } from "yargs";
|
|
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;
|
|
20
93
|
|
|
21
94
|
// Defer importing miniflare until we really need it. This takes ~0.5s
|
|
22
95
|
// and also modifies some `stream/web` and `undici` prototypes, so we
|
|
23
96
|
// don't want to do this if pages commands aren't being called.
|
|
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,30 +206,30 @@ async function spawnProxyProcess({
|
|
|
122
206
|
},
|
|
123
207
|
}
|
|
124
208
|
);
|
|
125
|
-
|
|
209
|
+
CLEANUP_CALLBACKS.push(() => {
|
|
126
210
|
proxy.kill();
|
|
127
211
|
});
|
|
128
212
|
|
|
129
213
|
proxy.stdout.on("data", (data) => {
|
|
130
|
-
|
|
214
|
+
logger.log(`[proxy]: ${data}`);
|
|
131
215
|
});
|
|
132
216
|
|
|
133
217
|
proxy.stderr.on("data", (data) => {
|
|
134
|
-
|
|
218
|
+
logger.error(`[proxy]: ${data}`);
|
|
135
219
|
});
|
|
136
220
|
|
|
137
221
|
proxy.on("close", (code) => {
|
|
138
|
-
|
|
222
|
+
logger.error(`Proxy exited with status ${code}.`);
|
|
139
223
|
});
|
|
140
224
|
|
|
141
225
|
// Wait for proxy process to start...
|
|
142
226
|
while (!proxy.pid) {}
|
|
143
227
|
|
|
144
228
|
if (port === undefined) {
|
|
145
|
-
|
|
229
|
+
logger.log(
|
|
146
230
|
`Sleeping ${SECONDS_TO_WAIT_FOR_PROXY} seconds to allow proxy process to start before attempting to automatically determine port...`
|
|
147
231
|
);
|
|
148
|
-
|
|
232
|
+
logger.log("To skip, specify the proxy port with --proxy.");
|
|
149
233
|
await sleep(SECONDS_TO_WAIT_FOR_PROXY * 1000);
|
|
150
234
|
|
|
151
235
|
port = getPids(proxy.pid)
|
|
@@ -153,12 +237,13 @@ async function spawnProxyProcess({
|
|
|
153
237
|
.filter((port) => port !== undefined)[0];
|
|
154
238
|
|
|
155
239
|
if (port === undefined) {
|
|
156
|
-
|
|
240
|
+
CLEANUP();
|
|
241
|
+
throw new FatalError(
|
|
157
242
|
"Could not automatically determine proxy port. Please specify the proxy port with --proxy.",
|
|
158
243
|
1
|
|
159
244
|
);
|
|
160
245
|
} else {
|
|
161
|
-
|
|
246
|
+
logger.log(`Automatically determined the proxy port to be ${port}.`);
|
|
162
247
|
}
|
|
163
248
|
}
|
|
164
249
|
|
|
@@ -215,7 +300,7 @@ function generateRulesMatcher<T>(
|
|
|
215
300
|
T
|
|
216
301
|
][];
|
|
217
302
|
|
|
218
|
-
return ({ request }: { request:
|
|
303
|
+
return ({ request }: { request: MiniflareRequest }) => {
|
|
219
304
|
const { pathname, host } = new URL(request.url);
|
|
220
305
|
|
|
221
306
|
return compiledRules
|
|
@@ -288,7 +373,7 @@ function generateHeadersMatcher(headersFile: string) {
|
|
|
288
373
|
)
|
|
289
374
|
);
|
|
290
375
|
|
|
291
|
-
return (request:
|
|
376
|
+
return (request: MiniflareRequest) => {
|
|
292
377
|
const matches = rulesMatcher({
|
|
293
378
|
request,
|
|
294
379
|
});
|
|
@@ -337,7 +422,7 @@ function generateRedirectsMatcher(redirectsFile: string) {
|
|
|
337
422
|
})
|
|
338
423
|
);
|
|
339
424
|
|
|
340
|
-
return (request:
|
|
425
|
+
return (request: MiniflareRequest) => {
|
|
341
426
|
const match = rulesMatcher({
|
|
342
427
|
request,
|
|
343
428
|
})[0];
|
|
@@ -392,7 +477,9 @@ function hasFileExtension(pathname: string) {
|
|
|
392
477
|
return /\/.+\.[a-z0-9]+$/i.test(pathname);
|
|
393
478
|
}
|
|
394
479
|
|
|
395
|
-
async function generateAssetsFetch(
|
|
480
|
+
async function generateAssetsFetch(
|
|
481
|
+
directory: string
|
|
482
|
+
): Promise<typeof miniflareFetch> {
|
|
396
483
|
// Defer importing miniflare until we really need it
|
|
397
484
|
const { Headers, Request, Response } = await import("@miniflare/core");
|
|
398
485
|
|
|
@@ -425,12 +512,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
|
|
|
425
512
|
}).on("change", (path) => {
|
|
426
513
|
switch (path) {
|
|
427
514
|
case headersFile: {
|
|
428
|
-
|
|
515
|
+
logger.log("_headers modified. Re-evaluating...");
|
|
429
516
|
headersMatcher = generateHeadersMatcher(headersFile);
|
|
430
517
|
break;
|
|
431
518
|
}
|
|
432
519
|
case redirectsFile: {
|
|
433
|
-
|
|
520
|
+
logger.log("_redirects modified. Re-evaluating...");
|
|
434
521
|
redirectsMatcher = generateRedirectsMatcher(redirectsFile);
|
|
435
522
|
break;
|
|
436
523
|
}
|
|
@@ -441,12 +528,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
|
|
|
441
528
|
return readFileSync(file);
|
|
442
529
|
};
|
|
443
530
|
|
|
444
|
-
const generateResponse = (request:
|
|
531
|
+
const generateResponse = (request: MiniflareRequest) => {
|
|
445
532
|
const url = new URL(request.url);
|
|
446
533
|
|
|
447
534
|
const deconstructedResponse: {
|
|
448
535
|
status: number;
|
|
449
|
-
headers:
|
|
536
|
+
headers: MiniflareHeaders;
|
|
450
537
|
body?: Buffer;
|
|
451
538
|
} = {
|
|
452
539
|
status: 200,
|
|
@@ -598,8 +685,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
|
|
|
598
685
|
};
|
|
599
686
|
|
|
600
687
|
const attachHeaders = (
|
|
601
|
-
request:
|
|
602
|
-
deconstructedResponse: {
|
|
688
|
+
request: MiniflareRequest,
|
|
689
|
+
deconstructedResponse: {
|
|
690
|
+
status: number;
|
|
691
|
+
headers: MiniflareHeaders;
|
|
692
|
+
body?: Buffer;
|
|
693
|
+
}
|
|
603
694
|
) => {
|
|
604
695
|
const headers = deconstructedResponse.headers;
|
|
605
696
|
const newHeaders = new Headers({});
|
|
@@ -643,7 +734,7 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
|
|
|
643
734
|
const RUNNING_BUILDERS: BuildResult[] = [];
|
|
644
735
|
|
|
645
736
|
async function buildFunctions({
|
|
646
|
-
|
|
737
|
+
outfile,
|
|
647
738
|
outputConfigPath,
|
|
648
739
|
functionsDirectory,
|
|
649
740
|
minify = false,
|
|
@@ -651,8 +742,11 @@ async function buildFunctions({
|
|
|
651
742
|
fallbackService = "ASSETS",
|
|
652
743
|
watch = false,
|
|
653
744
|
onEnd,
|
|
745
|
+
plugin = false,
|
|
746
|
+
buildOutputDirectory,
|
|
747
|
+
nodeCompat,
|
|
654
748
|
}: {
|
|
655
|
-
|
|
749
|
+
outfile: string;
|
|
656
750
|
outputConfigPath?: string;
|
|
657
751
|
functionsDirectory: string;
|
|
658
752
|
minify?: boolean;
|
|
@@ -660,12 +754,15 @@ async function buildFunctions({
|
|
|
660
754
|
fallbackService?: string;
|
|
661
755
|
watch?: boolean;
|
|
662
756
|
onEnd?: () => void;
|
|
757
|
+
plugin?: boolean;
|
|
758
|
+
buildOutputDirectory?: string;
|
|
759
|
+
nodeCompat?: boolean;
|
|
663
760
|
}) {
|
|
664
761
|
RUNNING_BUILDERS.forEach(
|
|
665
762
|
(runningBuilder) => runningBuilder.stop && runningBuilder.stop()
|
|
666
763
|
);
|
|
667
764
|
|
|
668
|
-
const routesModule = join(tmpdir(),
|
|
765
|
+
const routesModule = join(tmpdir(), `./functionsRoutes-${Math.random()}.mjs`);
|
|
669
766
|
const baseURL = toUrlPath("/");
|
|
670
767
|
|
|
671
768
|
const config: Config = await generateConfigFromFileTree({
|
|
@@ -686,23 +783,644 @@ async function buildFunctions({
|
|
|
686
783
|
outfile: routesModule,
|
|
687
784
|
});
|
|
688
785
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
786
|
+
if (plugin) {
|
|
787
|
+
RUNNING_BUILDERS.push(
|
|
788
|
+
await buildPlugin({
|
|
789
|
+
routesModule,
|
|
790
|
+
outfile,
|
|
791
|
+
minify,
|
|
792
|
+
sourcemap,
|
|
793
|
+
watch,
|
|
794
|
+
nodeCompat,
|
|
795
|
+
onEnd,
|
|
796
|
+
})
|
|
797
|
+
);
|
|
798
|
+
} else {
|
|
799
|
+
RUNNING_BUILDERS.push(
|
|
800
|
+
await buildWorker({
|
|
801
|
+
routesModule,
|
|
802
|
+
outfile,
|
|
803
|
+
minify,
|
|
804
|
+
sourcemap,
|
|
805
|
+
fallbackService,
|
|
806
|
+
watch,
|
|
807
|
+
onEnd,
|
|
808
|
+
buildOutputDirectory,
|
|
809
|
+
nodeCompat,
|
|
810
|
+
})
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
interface CreateDeploymentArgs {
|
|
816
|
+
directory: string;
|
|
817
|
+
projectName?: string;
|
|
818
|
+
branch?: string;
|
|
819
|
+
commitHash?: string;
|
|
820
|
+
commitMessage?: string;
|
|
821
|
+
commitDirty?: boolean;
|
|
700
822
|
}
|
|
701
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
|
+
|
|
702
1420
|
export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
703
1421
|
return yargs
|
|
704
1422
|
.command(
|
|
705
|
-
"dev [directory] [-- command]",
|
|
1423
|
+
"dev [directory] [-- command..]",
|
|
706
1424
|
"🧑💻 Develop your full-stack Pages application locally",
|
|
707
1425
|
(yargs) => {
|
|
708
1426
|
return yargs
|
|
@@ -758,8 +1476,15 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
758
1476
|
default: false,
|
|
759
1477
|
description: "Auto reload HTML pages when change is detected",
|
|
760
1478
|
},
|
|
1479
|
+
"node-compat": {
|
|
1480
|
+
describe: "Enable node.js compatibility",
|
|
1481
|
+
default: false,
|
|
1482
|
+
type: "boolean",
|
|
1483
|
+
hidden: true,
|
|
1484
|
+
},
|
|
761
1485
|
// TODO: Miniflare user options
|
|
762
|
-
})
|
|
1486
|
+
})
|
|
1487
|
+
.epilogue(pagesBetaWarning);
|
|
763
1488
|
},
|
|
764
1489
|
async ({
|
|
765
1490
|
local,
|
|
@@ -771,10 +1496,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
771
1496
|
kv: kvs = [],
|
|
772
1497
|
do: durableObjects = [],
|
|
773
1498
|
"live-reload": liveReload,
|
|
774
|
-
"
|
|
1499
|
+
"node-compat": nodeCompat,
|
|
1500
|
+
_: [_pages, _dev, ...remaining],
|
|
775
1501
|
}) => {
|
|
1502
|
+
// Beta message for `wrangler pages <commands>` usage
|
|
1503
|
+
logger.log(pagesBetaWarning);
|
|
1504
|
+
|
|
776
1505
|
if (!local) {
|
|
777
|
-
|
|
1506
|
+
logger.error("Only local mode is supported at the moment.");
|
|
778
1507
|
return;
|
|
779
1508
|
}
|
|
780
1509
|
|
|
@@ -795,41 +1524,59 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
795
1524
|
|
|
796
1525
|
let miniflareArgs: MiniflareOptions = {};
|
|
797
1526
|
|
|
798
|
-
let scriptReadyResolve;
|
|
799
|
-
const scriptReadyPromise = new Promise(
|
|
1527
|
+
let scriptReadyResolve: () => void;
|
|
1528
|
+
const scriptReadyPromise = new Promise<void>(
|
|
800
1529
|
(resolve) => (scriptReadyResolve = resolve)
|
|
801
1530
|
);
|
|
802
1531
|
|
|
803
1532
|
if (usingFunctions) {
|
|
804
|
-
const
|
|
1533
|
+
const outfile = join(
|
|
1534
|
+
tmpdir(),
|
|
1535
|
+
`./functionsWorker-${Math.random()}.js`
|
|
1536
|
+
);
|
|
805
1537
|
|
|
806
|
-
|
|
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
|
+
}
|
|
807
1543
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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 {}
|
|
815
1557
|
|
|
816
1558
|
watch([functionsDirectory], {
|
|
817
1559
|
persistent: true,
|
|
818
1560
|
ignoreInitial: true,
|
|
819
1561
|
}).on("all", async () => {
|
|
820
1562
|
await buildFunctions({
|
|
821
|
-
|
|
1563
|
+
outfile,
|
|
822
1564
|
functionsDirectory,
|
|
823
1565
|
sourcemap: true,
|
|
824
1566
|
watch: true,
|
|
825
1567
|
onEnd: () => scriptReadyResolve(),
|
|
1568
|
+
buildOutputDirectory: directory,
|
|
1569
|
+
nodeCompat,
|
|
826
1570
|
});
|
|
827
1571
|
});
|
|
828
1572
|
|
|
829
1573
|
miniflareArgs = {
|
|
830
|
-
scriptPath,
|
|
1574
|
+
scriptPath: outfile,
|
|
831
1575
|
};
|
|
832
1576
|
} else {
|
|
1577
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1578
|
+
scriptReadyResolve!();
|
|
1579
|
+
|
|
833
1580
|
const scriptPath =
|
|
834
1581
|
directory !== undefined
|
|
835
1582
|
? join(directory, singleWorkerScriptPath)
|
|
@@ -840,8 +1587,11 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
840
1587
|
scriptPath,
|
|
841
1588
|
};
|
|
842
1589
|
} else {
|
|
843
|
-
|
|
1590
|
+
logger.log("No functions. Shimming...");
|
|
844
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,
|
|
845
1595
|
// TODO: The fact that these request/response hacks are necessary is ridiculous.
|
|
846
1596
|
// We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
|
|
847
1597
|
script: `
|
|
@@ -852,7 +1602,6 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
852
1602
|
}
|
|
853
1603
|
}`,
|
|
854
1604
|
};
|
|
855
|
-
scriptReadyResolve();
|
|
856
1605
|
}
|
|
857
1606
|
}
|
|
858
1607
|
|
|
@@ -873,6 +1622,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
873
1622
|
? await generateAssetsFetch(directory)
|
|
874
1623
|
: invalidAssetsFetch;
|
|
875
1624
|
|
|
1625
|
+
const requestContextCheckOptions =
|
|
1626
|
+
await getRequestContextCheckOptions();
|
|
1627
|
+
|
|
876
1628
|
const miniflare = new Miniflare({
|
|
877
1629
|
port,
|
|
878
1630
|
watch: true,
|
|
@@ -893,20 +1645,22 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
893
1645
|
// User bindings
|
|
894
1646
|
bindings: {
|
|
895
1647
|
...Object.fromEntries(
|
|
896
|
-
bindings
|
|
1648
|
+
bindings
|
|
1649
|
+
.map((binding) => binding.toString().split("="))
|
|
1650
|
+
.map(([key, ...values]) => [key, values.join("=")])
|
|
897
1651
|
),
|
|
898
1652
|
},
|
|
899
1653
|
|
|
900
1654
|
// env.ASSETS.fetch
|
|
901
1655
|
serviceBindings: {
|
|
902
|
-
async ASSETS(request) {
|
|
1656
|
+
async ASSETS(request: MiniflareRequest) {
|
|
903
1657
|
if (proxyPort) {
|
|
904
1658
|
try {
|
|
905
1659
|
const url = new URL(request.url);
|
|
906
1660
|
url.host = `localhost:${proxyPort}`;
|
|
907
1661
|
return await fetch(url, request);
|
|
908
1662
|
} catch (thrown) {
|
|
909
|
-
|
|
1663
|
+
logger.error(`Could not proxy request: ${thrown}`);
|
|
910
1664
|
|
|
911
1665
|
// TODO: Pretty error page
|
|
912
1666
|
return new Response(
|
|
@@ -918,7 +1672,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
918
1672
|
try {
|
|
919
1673
|
return await assetsFetch(request);
|
|
920
1674
|
} catch (thrown) {
|
|
921
|
-
|
|
1675
|
+
logger.error(`Could not serve static asset: ${thrown}`);
|
|
922
1676
|
|
|
923
1677
|
// TODO: Pretty error page
|
|
924
1678
|
return new Response(
|
|
@@ -935,18 +1689,17 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
935
1689
|
cachePersist: true,
|
|
936
1690
|
liveReload,
|
|
937
1691
|
|
|
1692
|
+
...requestContextCheckOptions,
|
|
938
1693
|
...miniflareArgs,
|
|
939
1694
|
});
|
|
940
1695
|
|
|
941
1696
|
try {
|
|
942
1697
|
// `startServer` might throw if user code contains errors
|
|
943
1698
|
const server = await miniflare.startServer();
|
|
944
|
-
|
|
1699
|
+
logger.log(`Serving at http://localhost:${port}/`);
|
|
945
1700
|
|
|
946
1701
|
if (process.env.BROWSER !== "none") {
|
|
947
|
-
|
|
948
|
-
// fail silently if the open command doesn't work (e.g. in GitHub Codespaces)
|
|
949
|
-
childProcess.on("error", (_err) => {});
|
|
1702
|
+
await openInBrowser(`http://localhost:${port}/`);
|
|
950
1703
|
}
|
|
951
1704
|
|
|
952
1705
|
if (directory !== undefined && liveReload) {
|
|
@@ -958,13 +1711,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
958
1711
|
});
|
|
959
1712
|
}
|
|
960
1713
|
|
|
961
|
-
|
|
1714
|
+
CLEANUP_CALLBACKS.push(() => {
|
|
962
1715
|
server.close();
|
|
963
1716
|
miniflare.dispose().catch((err) => miniflare.log.error(err));
|
|
964
1717
|
});
|
|
965
1718
|
} catch (e) {
|
|
966
|
-
miniflare.log.error(e);
|
|
967
|
-
|
|
1719
|
+
miniflare.log.error(e as Error);
|
|
1720
|
+
CLEANUP();
|
|
1721
|
+
throw new FatalError("Could not start Miniflare.", 1);
|
|
968
1722
|
}
|
|
969
1723
|
}
|
|
970
1724
|
)
|
|
@@ -982,7 +1736,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
982
1736
|
description: "The directory of Pages Functions",
|
|
983
1737
|
})
|
|
984
1738
|
.options({
|
|
985
|
-
|
|
1739
|
+
outfile: {
|
|
986
1740
|
type: "string",
|
|
987
1741
|
default: "_worker.js",
|
|
988
1742
|
description: "The location of the output Worker script",
|
|
@@ -1014,32 +1768,326 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
|
|
|
1014
1768
|
description:
|
|
1015
1769
|
"Watch for changes to the functions and automatically rebuild the Worker script",
|
|
1016
1770
|
},
|
|
1017
|
-
|
|
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),
|
|
1018
1788
|
async ({
|
|
1019
1789
|
directory,
|
|
1020
|
-
|
|
1790
|
+
outfile,
|
|
1021
1791
|
"output-config-path": outputConfigPath,
|
|
1022
1792
|
minify,
|
|
1023
1793
|
sourcemap,
|
|
1024
1794
|
fallbackService,
|
|
1025
1795
|
watch,
|
|
1796
|
+
plugin,
|
|
1797
|
+
"build-output-directory": buildOutputDirectory,
|
|
1798
|
+
"node-compat": nodeCompat,
|
|
1026
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
|
+
|
|
1027
1813
|
await buildFunctions({
|
|
1028
|
-
|
|
1814
|
+
outfile,
|
|
1029
1815
|
outputConfigPath,
|
|
1030
1816
|
functionsDirectory: directory,
|
|
1031
1817
|
minify,
|
|
1032
1818
|
sourcemap,
|
|
1033
1819
|
fallbackService,
|
|
1034
1820
|
watch,
|
|
1821
|
+
plugin,
|
|
1822
|
+
buildOutputDirectory,
|
|
1823
|
+
nodeCompat,
|
|
1035
1824
|
});
|
|
1036
1825
|
}
|
|
1037
1826
|
)
|
|
1038
|
-
)
|
|
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);
|
|
1039
2046
|
};
|
|
1040
2047
|
|
|
1041
|
-
const invalidAssetsFetch: typeof
|
|
2048
|
+
const invalidAssetsFetch: typeof miniflareFetch = () => {
|
|
1042
2049
|
throw new Error(
|
|
1043
2050
|
"Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
|
|
1044
2051
|
);
|
|
1045
2052
|
};
|
|
2053
|
+
|
|
2054
|
+
const listProjects = async ({
|
|
2055
|
+
accountId,
|
|
2056
|
+
}: {
|
|
2057
|
+
accountId: string;
|
|
2058
|
+
}): Promise<Array<Project>> => {
|
|
2059
|
+
const pageSize = 10;
|
|
2060
|
+
let page = 1;
|
|
2061
|
+
const results = [];
|
|
2062
|
+
while (results.length % pageSize === 0) {
|
|
2063
|
+
const json: Array<Project> = await fetchResult(
|
|
2064
|
+
`/accounts/${accountId}/pages/projects`,
|
|
2065
|
+
{},
|
|
2066
|
+
new URLSearchParams({
|
|
2067
|
+
per_page: pageSize.toString(),
|
|
2068
|
+
page: page.toString(),
|
|
2069
|
+
})
|
|
2070
|
+
);
|
|
2071
|
+
page++;
|
|
2072
|
+
results.push(...json);
|
|
2073
|
+
if (json.length < pageSize) {
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return results;
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
function formatTime(duration: number) {
|
|
2081
|
+
return `(${(duration / 1000).toFixed(2)} sec)`;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
function Progress({ done, total }: { done: number; total: number }) {
|
|
2085
|
+
return (
|
|
2086
|
+
<>
|
|
2087
|
+
<Text>
|
|
2088
|
+
<Spinner type="earth" />
|
|
2089
|
+
{` Uploading... (${done}/${total})\n`}
|
|
2090
|
+
</Text>
|
|
2091
|
+
</>
|
|
2092
|
+
);
|
|
2093
|
+
}
|