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