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