wrangler 2.0.23 → 2.0.24
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/README.md +20 -2
- package/bin/wrangler.js +1 -1
- package/miniflare-dist/index.mjs +96 -34
- package/package.json +9 -4
- package/src/__tests__/configuration.test.ts +88 -16
- package/src/__tests__/dev.test.tsx +3 -0
- package/src/__tests__/generate.test.ts +93 -0
- package/src/__tests__/helpers/mock-cfetch.ts +54 -2
- package/src/__tests__/index.test.ts +10 -27
- package/src/__tests__/jest.setup.ts +31 -1
- package/src/__tests__/kv.test.ts +2 -2
- package/src/__tests__/metrics.test.ts +5 -0
- package/src/__tests__/publish.test.ts +497 -254
- package/src/__tests__/r2.test.ts +155 -71
- package/src/__tests__/user.test.ts +1 -0
- package/src/__tests__/validate-dev-props.test.ts +56 -0
- package/src/__tests__/whoami.test.tsx +60 -1
- package/src/bundle.ts +278 -44
- package/src/cfetch/internal.ts +34 -2
- package/src/config/config.ts +7 -2
- package/src/config/environment.ts +40 -8
- package/src/config/index.ts +13 -0
- package/src/config/validation.ts +101 -7
- package/src/create-worker-upload-form.ts +25 -0
- package/src/dev/dev.tsx +107 -28
- package/src/dev/local.tsx +20 -10
- package/src/dev/remote.tsx +39 -8
- package/src/dev/use-esbuild.ts +25 -0
- package/src/dev/validate-dev-props.ts +31 -0
- package/src/dev-registry.tsx +157 -0
- package/src/dev.tsx +93 -75
- package/src/generate.ts +112 -14
- package/src/index.tsx +182 -4
- package/src/inspect.ts +93 -5
- package/src/metrics/index.ts +1 -0
- package/src/metrics/metrics-dispatcher.ts +1 -0
- package/src/metrics/metrics-usage-headers.ts +24 -0
- package/src/metrics/send-event.ts +2 -2
- package/src/module-collection.ts +3 -3
- package/src/pages/constants.ts +1 -0
- package/src/pages/deployments.tsx +1 -1
- package/src/pages/dev.tsx +6 -1
- package/src/pages/publish.tsx +1 -1
- package/src/pages/upload.tsx +32 -13
- package/src/publish.ts +126 -112
- package/src/r2.ts +68 -0
- package/src/user/user.tsx +20 -2
- package/src/whoami.tsx +79 -1
- package/src/worker.ts +12 -0
- package/templates/first-party-worker-module-facade.ts +18 -0
- package/templates/format-dev-errors.ts +32 -0
- package/templates/{static-asset-facade.js → serve-static-assets.ts} +21 -7
- package/templates/service-bindings-module-facade.js +51 -0
- package/templates/service-bindings-sw-facade.js +39 -0
- package/wrangler-dist/cli.js +40192 -15265
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import * as stream from "node:stream";
|
|
2
4
|
import { StringDecoder } from "node:string_decoder";
|
|
3
5
|
import { setTimeout } from "node:timers/promises";
|
|
4
6
|
import TOML from "@iarna/toml";
|
|
@@ -45,7 +47,14 @@ import {
|
|
|
45
47
|
import { previewHandler, previewOptions } from "./preview";
|
|
46
48
|
import publish from "./publish";
|
|
47
49
|
import { pubSubCommands } from "./pubsub/pubsub-commands";
|
|
48
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
bucketAndKeyFromObjectPath,
|
|
52
|
+
createR2Bucket,
|
|
53
|
+
deleteR2Bucket,
|
|
54
|
+
getR2Object,
|
|
55
|
+
listR2Buckets,
|
|
56
|
+
putR2Object,
|
|
57
|
+
} from "./r2";
|
|
49
58
|
import { getAssetPaths, getSiteAssetPaths } from "./sites";
|
|
50
59
|
import {
|
|
51
60
|
createTail,
|
|
@@ -67,6 +76,7 @@ import { workerNamespaceCommands } from "./worker-namespace";
|
|
|
67
76
|
import type { Config } from "./config";
|
|
68
77
|
import type { KeyValue } from "./kv";
|
|
69
78
|
import type { TailCLIFilters } from "./tail";
|
|
79
|
+
import type { Readable } from "node:stream";
|
|
70
80
|
import type { RawData } from "ws";
|
|
71
81
|
import type { CommandModule } from "yargs";
|
|
72
82
|
import type Yargs from "yargs";
|
|
@@ -513,10 +523,16 @@ function createCLIParser(argv: string[]) {
|
|
|
513
523
|
(args.config as ConfigPath) ||
|
|
514
524
|
(args.script && findWranglerToml(path.dirname(args.script)));
|
|
515
525
|
const config = readConfig(configPath, args);
|
|
516
|
-
await metrics.sendMetricsEvent("deploy worker script", {
|
|
517
|
-
sendMetrics: config.send_metrics,
|
|
518
|
-
});
|
|
519
526
|
const entry = await getEntry(args, config, "publish");
|
|
527
|
+
await metrics.sendMetricsEvent(
|
|
528
|
+
"deploy worker script",
|
|
529
|
+
{
|
|
530
|
+
usesTypeScript: /\.tsx?$/.test(entry.file),
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
sendMetrics: config.send_metrics,
|
|
534
|
+
}
|
|
535
|
+
);
|
|
520
536
|
|
|
521
537
|
if (args.public) {
|
|
522
538
|
throw new Error("The --public field has been renamed to --assets");
|
|
@@ -950,6 +966,7 @@ function createCLIParser(argv: string[]) {
|
|
|
950
966
|
text_blobs: {},
|
|
951
967
|
data_blobs: {},
|
|
952
968
|
worker_namespaces: [],
|
|
969
|
+
logfwdr: { schema: undefined, bindings: [] },
|
|
953
970
|
unsafe: [],
|
|
954
971
|
},
|
|
955
972
|
modules: [],
|
|
@@ -1746,6 +1763,167 @@ function createCLIParser(argv: string[]) {
|
|
|
1746
1763
|
wrangler.command("r2", "📦 Interact with an R2 store", (r2Yargs) => {
|
|
1747
1764
|
return r2Yargs
|
|
1748
1765
|
.command(subHelp)
|
|
1766
|
+
.command("object", "Manage R2 objects", (r2ObjectYargs) => {
|
|
1767
|
+
return r2ObjectYargs
|
|
1768
|
+
.command(
|
|
1769
|
+
"get <objectPath>",
|
|
1770
|
+
"Fetch an object from an R2 bucket",
|
|
1771
|
+
(yargs) => {
|
|
1772
|
+
return yargs
|
|
1773
|
+
.positional("objectPath", {
|
|
1774
|
+
describe:
|
|
1775
|
+
"The source object path in the form of {bucket}/{key}",
|
|
1776
|
+
type: "string",
|
|
1777
|
+
})
|
|
1778
|
+
.option("file", {
|
|
1779
|
+
describe: "The destination file to create",
|
|
1780
|
+
alias: "f",
|
|
1781
|
+
conflicts: "pipe",
|
|
1782
|
+
requiresArg: true,
|
|
1783
|
+
type: "string",
|
|
1784
|
+
})
|
|
1785
|
+
.option("pipe", {
|
|
1786
|
+
describe:
|
|
1787
|
+
"Enables the file to be piped to a destination, rather than specified with the --file option",
|
|
1788
|
+
alias: "p",
|
|
1789
|
+
conflicts: "file",
|
|
1790
|
+
type: "boolean",
|
|
1791
|
+
});
|
|
1792
|
+
},
|
|
1793
|
+
async (args) => {
|
|
1794
|
+
const config = readConfig(args.config as ConfigPath, args);
|
|
1795
|
+
const accountId = await requireAuth(config);
|
|
1796
|
+
const { objectPath, pipe } = args;
|
|
1797
|
+
const { bucket, key } = bucketAndKeyFromObjectPath(objectPath);
|
|
1798
|
+
|
|
1799
|
+
let file = args.file;
|
|
1800
|
+
if (!file && !pipe) {
|
|
1801
|
+
file = key;
|
|
1802
|
+
}
|
|
1803
|
+
if (!pipe) {
|
|
1804
|
+
await printWranglerBanner();
|
|
1805
|
+
logger.log(`Downloading "${key}" from "${bucket}".`);
|
|
1806
|
+
}
|
|
1807
|
+
const input = await getR2Object(accountId, bucket, key);
|
|
1808
|
+
const output = file ? fs.createWriteStream(file) : process.stdout;
|
|
1809
|
+
await new Promise<void>((resolve, reject) => {
|
|
1810
|
+
stream.pipeline(input, output, (err: unknown) => {
|
|
1811
|
+
err ? reject(err) : resolve();
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
if (!pipe) logger.log("Download complete.");
|
|
1815
|
+
}
|
|
1816
|
+
)
|
|
1817
|
+
.command(
|
|
1818
|
+
"put <objectPath>",
|
|
1819
|
+
"Create an object in an R2 bucket",
|
|
1820
|
+
(yargs) => {
|
|
1821
|
+
return yargs
|
|
1822
|
+
.positional("objectPath", {
|
|
1823
|
+
describe:
|
|
1824
|
+
"The destination object path in the form of {bucket}/{key}",
|
|
1825
|
+
type: "string",
|
|
1826
|
+
})
|
|
1827
|
+
.option("file", {
|
|
1828
|
+
describe: "The path of the file to upload",
|
|
1829
|
+
alias: "f",
|
|
1830
|
+
conflicts: "pipe",
|
|
1831
|
+
requiresArg: true,
|
|
1832
|
+
type: "string",
|
|
1833
|
+
})
|
|
1834
|
+
.option("pipe", {
|
|
1835
|
+
describe:
|
|
1836
|
+
"Enables the file to be piped in, rather than specified with the --file option",
|
|
1837
|
+
alias: "p",
|
|
1838
|
+
conflicts: "file",
|
|
1839
|
+
type: "boolean",
|
|
1840
|
+
})
|
|
1841
|
+
.option("content-type", {
|
|
1842
|
+
describe:
|
|
1843
|
+
"A standard MIME type describing the format of the object data",
|
|
1844
|
+
alias: "ct",
|
|
1845
|
+
requiresArg: true,
|
|
1846
|
+
type: "string",
|
|
1847
|
+
})
|
|
1848
|
+
.option("content-disposition", {
|
|
1849
|
+
describe:
|
|
1850
|
+
"Specifies presentational information for the object",
|
|
1851
|
+
alias: "cd",
|
|
1852
|
+
requiresArg: true,
|
|
1853
|
+
type: "string",
|
|
1854
|
+
})
|
|
1855
|
+
.option("content-encoding", {
|
|
1856
|
+
describe:
|
|
1857
|
+
"Specifies what content encodings have been applied to the object and thus what decoding mechanisms must be applied to obtain the media-type referenced by the Content-Type header field",
|
|
1858
|
+
alias: "ce",
|
|
1859
|
+
requiresArg: true,
|
|
1860
|
+
type: "string",
|
|
1861
|
+
})
|
|
1862
|
+
.option("content-language", {
|
|
1863
|
+
describe: "The language the content is in",
|
|
1864
|
+
alias: "cl",
|
|
1865
|
+
requiresArg: true,
|
|
1866
|
+
type: "string",
|
|
1867
|
+
})
|
|
1868
|
+
.option("cache-control", {
|
|
1869
|
+
describe:
|
|
1870
|
+
"Specifies caching behavior along the request/reply chain",
|
|
1871
|
+
alias: "cc",
|
|
1872
|
+
requiresArg: true,
|
|
1873
|
+
type: "string",
|
|
1874
|
+
})
|
|
1875
|
+
.option("expires", {
|
|
1876
|
+
describe:
|
|
1877
|
+
"The date and time at which the object is no longer cacheable",
|
|
1878
|
+
alias: "e",
|
|
1879
|
+
requiresArg: true,
|
|
1880
|
+
type: "string",
|
|
1881
|
+
});
|
|
1882
|
+
},
|
|
1883
|
+
async (args) => {
|
|
1884
|
+
await printWranglerBanner();
|
|
1885
|
+
|
|
1886
|
+
const config = readConfig(args.config as ConfigPath, args);
|
|
1887
|
+
const accountId = await requireAuth(config);
|
|
1888
|
+
const { objectPath, file, pipe, ...options } = args;
|
|
1889
|
+
const { bucket, key } = bucketAndKeyFromObjectPath(objectPath);
|
|
1890
|
+
if (!file && !pipe) {
|
|
1891
|
+
throw new CommandLineArgsError(
|
|
1892
|
+
"Either the --file or --pipe options are required."
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
let object: Readable | Buffer;
|
|
1896
|
+
let objectSize: number;
|
|
1897
|
+
if (file) {
|
|
1898
|
+
object = fs.createReadStream(file);
|
|
1899
|
+
const stats = fs.statSync(file);
|
|
1900
|
+
objectSize = stats.size;
|
|
1901
|
+
} else {
|
|
1902
|
+
object = await new Promise<Buffer>((resolve, reject) => {
|
|
1903
|
+
const stdin = process.stdin;
|
|
1904
|
+
const chunks = Array<Buffer>();
|
|
1905
|
+
stdin.on("data", (chunk) => chunks.push(chunk));
|
|
1906
|
+
stdin.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1907
|
+
stdin.on("error", (err) =>
|
|
1908
|
+
reject(
|
|
1909
|
+
new CommandLineArgsError(
|
|
1910
|
+
`Could not pipe. Reason: "${err.message}"`
|
|
1911
|
+
)
|
|
1912
|
+
)
|
|
1913
|
+
);
|
|
1914
|
+
});
|
|
1915
|
+
objectSize = object.byteLength;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
logger.log(`Creating object "${key}" in bucket "${bucket}".`);
|
|
1919
|
+
await putR2Object(accountId, bucket, key, object, {
|
|
1920
|
+
...options,
|
|
1921
|
+
"content-length": `${objectSize}`,
|
|
1922
|
+
});
|
|
1923
|
+
logger.log("Upload complete.");
|
|
1924
|
+
}
|
|
1925
|
+
);
|
|
1926
|
+
})
|
|
1749
1927
|
.command("bucket", "Manage R2 buckets", (r2BucketYargs) => {
|
|
1750
1928
|
r2BucketYargs.command(
|
|
1751
1929
|
"create <name>",
|
package/src/inspect.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
1
2
|
import assert from "node:assert";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
4
|
import os from "node:os";
|
|
@@ -5,6 +6,7 @@ import { URL } from "node:url";
|
|
|
5
6
|
|
|
6
7
|
import open from "open";
|
|
7
8
|
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { SourceMapConsumer } from "source-map";
|
|
8
10
|
import WebSocket, { WebSocketServer } from "ws";
|
|
9
11
|
import { version } from "../package.json";
|
|
10
12
|
import { logger } from "./logger";
|
|
@@ -52,6 +54,10 @@ interface InspectorProps {
|
|
|
52
54
|
* logged to the terminal by nature of them actually running in node locally.)
|
|
53
55
|
*/
|
|
54
56
|
logToTerminal: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Sourcemap path, so that stacktraces can be interpretted
|
|
59
|
+
*/
|
|
60
|
+
sourceMapPath?: string | undefined;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export default function useInspector(props: InspectorProps) {
|
|
@@ -252,15 +258,96 @@ export default function useInspector(props: InspectorProps) {
|
|
|
252
258
|
* without having to open the devtools).
|
|
253
259
|
*/
|
|
254
260
|
if (props.logToTerminal) {
|
|
255
|
-
ws.addEventListener("message", (event: MessageEvent) => {
|
|
261
|
+
ws.addEventListener("message", async (event: MessageEvent) => {
|
|
256
262
|
if (typeof event.data === "string") {
|
|
257
263
|
const evt = JSON.parse(event.data);
|
|
258
264
|
if (evt.method === "Runtime.exceptionThrown") {
|
|
259
265
|
const params = evt.params as Protocol.Runtime.ExceptionThrownEvent;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
266
|
+
|
|
267
|
+
// Parse stack trace with source map.
|
|
268
|
+
if (props.sourceMapPath) {
|
|
269
|
+
// Parse in the sourcemap
|
|
270
|
+
const mapContent = JSON.parse(
|
|
271
|
+
await readFile(props.sourceMapPath, "utf-8")
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Create the lines for the exception details log
|
|
275
|
+
const exceptionLines = [
|
|
276
|
+
params.exceptionDetails.exception?.description?.split("\n")[0],
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
await SourceMapConsumer.with(
|
|
280
|
+
mapContent,
|
|
281
|
+
null,
|
|
282
|
+
async (consumer) => {
|
|
283
|
+
// Pass each of the callframes into the consumer, and format the error
|
|
284
|
+
const stack = params.exceptionDetails.stackTrace?.callFrames;
|
|
285
|
+
|
|
286
|
+
stack?.forEach(
|
|
287
|
+
({ functionName, lineNumber, columnNumber }, i) => {
|
|
288
|
+
try {
|
|
289
|
+
if (lineNumber) {
|
|
290
|
+
// The line and column numbers in the stackTrace are zero indexed,
|
|
291
|
+
// whereas the sourcemap consumer indexes from one.
|
|
292
|
+
const pos = consumer.originalPositionFor({
|
|
293
|
+
line: lineNumber + 1,
|
|
294
|
+
column: columnNumber + 1,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Print out line which caused error:
|
|
298
|
+
if (i === 0 && pos.source && pos.line) {
|
|
299
|
+
const fileSource = consumer.sourceContentFor(
|
|
300
|
+
pos.source
|
|
301
|
+
);
|
|
302
|
+
const fileSourceLine =
|
|
303
|
+
fileSource?.split("\n")[pos.line - 1] || "";
|
|
304
|
+
exceptionLines.push(fileSourceLine.trim());
|
|
305
|
+
|
|
306
|
+
// If we have a column, we can mark the position underneath
|
|
307
|
+
if (pos.column) {
|
|
308
|
+
exceptionLines.push(
|
|
309
|
+
`${" ".repeat(
|
|
310
|
+
pos.column - fileSourceLine.search(/\S/)
|
|
311
|
+
)}^`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// From the way esbuild implements the "names" field:
|
|
317
|
+
// > To save space, the original name is only recorded when it's different from the final name.
|
|
318
|
+
// however, source-map consumer does not handle this
|
|
319
|
+
if (pos && pos.line != null) {
|
|
320
|
+
const convertedFnName =
|
|
321
|
+
pos.name || functionName || "";
|
|
322
|
+
exceptionLines.push(
|
|
323
|
+
` at ${convertedFnName} (${pos.source?.replace(
|
|
324
|
+
`${mapContent.sourceRoot}/`,
|
|
325
|
+
""
|
|
326
|
+
)}:${pos.line}:${pos.column})`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
// Line failed to parse through the sourcemap consumer
|
|
332
|
+
// We should handle this better
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Log the parsed stacktrace
|
|
340
|
+
logger.error(
|
|
341
|
+
params.exceptionDetails.text,
|
|
342
|
+
exceptionLines.join("\n")
|
|
343
|
+
);
|
|
344
|
+
} else {
|
|
345
|
+
// We log the stacktrace to the terminal
|
|
346
|
+
logger.error(
|
|
347
|
+
params.exceptionDetails.text,
|
|
348
|
+
params.exceptionDetails.exception?.description ?? ""
|
|
349
|
+
);
|
|
350
|
+
}
|
|
264
351
|
}
|
|
265
352
|
if (evt.method === "Runtime.consoleAPICalled") {
|
|
266
353
|
logConsoleMessage(
|
|
@@ -333,6 +420,7 @@ export default function useInspector(props: InspectorProps) {
|
|
|
333
420
|
}, [
|
|
334
421
|
props.inspectorUrl,
|
|
335
422
|
props.logToTerminal,
|
|
423
|
+
props.sourceMapPath,
|
|
336
424
|
wsServer,
|
|
337
425
|
// We use a state value as a sigil to trigger a retry of the
|
|
338
426
|
// remote websocket connection. It's not used inside the effect,
|
package/src/metrics/index.ts
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getMetricsConfig } from "./metrics-config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add an additional header to publish requests if the user has opted into sending usage metrics.
|
|
5
|
+
*
|
|
6
|
+
* This allows us to estimate the number of instances of Wrangler that have opted-in
|
|
7
|
+
* without breaking our agreement not to send stuff if you have not opted-in.
|
|
8
|
+
*/
|
|
9
|
+
export async function getMetricsUsageHeaders(
|
|
10
|
+
sendMetrics: boolean | undefined
|
|
11
|
+
): Promise<Record<string, string> | undefined> {
|
|
12
|
+
const metricsEnabled = (
|
|
13
|
+
await getMetricsConfig({
|
|
14
|
+
sendMetrics,
|
|
15
|
+
})
|
|
16
|
+
).enabled;
|
|
17
|
+
if (metricsEnabled) {
|
|
18
|
+
return {
|
|
19
|
+
metricsEnabled: "true",
|
|
20
|
+
};
|
|
21
|
+
} else {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -47,8 +47,8 @@ export type EventNames =
|
|
|
47
47
|
| "rename worker namespace"
|
|
48
48
|
| "create pages project"
|
|
49
49
|
| "list pages projects"
|
|
50
|
-
| "
|
|
51
|
-
| "list pages
|
|
50
|
+
| "create pages deployment"
|
|
51
|
+
| "list pages deployments"
|
|
52
52
|
| "build pages functions"
|
|
53
53
|
| "run dev"
|
|
54
54
|
| "run pages dev";
|
package/src/module-collection.ts
CHANGED
|
@@ -123,9 +123,9 @@ export default function createModuleCollector(props: {
|
|
|
123
123
|
{
|
|
124
124
|
filter: new RegExp(
|
|
125
125
|
"^(" +
|
|
126
|
-
[...props.wrangler1xlegacyModuleReferences.fileNames]
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
[...props.wrangler1xlegacyModuleReferences.fileNames]
|
|
127
|
+
.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
128
|
+
.join("|") +
|
|
129
129
|
")$"
|
|
130
130
|
),
|
|
131
131
|
},
|
package/src/pages/constants.ts
CHANGED
|
@@ -3,5 +3,6 @@ export const MAX_BUCKET_SIZE = 50 * 1024 * 1024;
|
|
|
3
3
|
export const MAX_BUCKET_FILE_COUNT = 5000;
|
|
4
4
|
export const BULK_UPLOAD_CONCURRENCY = 3;
|
|
5
5
|
export const MAX_UPLOAD_ATTEMPTS = 5;
|
|
6
|
+
export const MAX_CHECK_MISSING_ATTEMPTS = 5;
|
|
6
7
|
export const SECONDS_TO_WAIT_FOR_PROXY = 5;
|
|
7
8
|
export const isInPagesCI = !!process.env.CF_PAGES;
|
package/src/pages/dev.tsx
CHANGED
|
@@ -139,7 +139,12 @@ export const Handler = async ({
|
|
|
139
139
|
|
|
140
140
|
let proxyPort: number | undefined;
|
|
141
141
|
|
|
142
|
-
if (directory
|
|
142
|
+
if (directory !== undefined && command.length > 0) {
|
|
143
|
+
throw new FatalError(
|
|
144
|
+
"Specify either a directory OR a proxy command, not both.",
|
|
145
|
+
1
|
|
146
|
+
);
|
|
147
|
+
} else if (directory === undefined) {
|
|
143
148
|
proxyPort = await spawnProxyProcess({
|
|
144
149
|
port: requestedProxyPort,
|
|
145
150
|
command,
|
package/src/pages/publish.tsx
CHANGED
package/src/pages/upload.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
BULK_UPLOAD_CONCURRENCY,
|
|
23
23
|
MAX_BUCKET_FILE_COUNT,
|
|
24
24
|
MAX_BUCKET_SIZE,
|
|
25
|
+
MAX_CHECK_MISSING_ATTEMPTS,
|
|
25
26
|
MAX_UPLOAD_ATTEMPTS,
|
|
26
27
|
} from "./constants";
|
|
27
28
|
import { pagesBetaWarning } from "./utils";
|
|
@@ -190,19 +191,37 @@ export const upload = async (
|
|
|
190
191
|
|
|
191
192
|
const start = Date.now();
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
let attempts = 0;
|
|
195
|
+
const getMissingHashes = async (): Promise<string[]> => {
|
|
196
|
+
try {
|
|
197
|
+
return await fetchResult<string[]>(`/pages/assets/check-missing`, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
Authorization: `Bearer ${jwt}`,
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
hashes: files.map(({ hash }) => hash),
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
} catch (e) {
|
|
208
|
+
if (attempts < MAX_CHECK_MISSING_ATTEMPTS) {
|
|
209
|
+
// Exponential backoff, 1 second first time, then 2 second, then 4 second etc.
|
|
210
|
+
await new Promise((resolvePromise) =>
|
|
211
|
+
setTimeout(resolvePromise, Math.pow(2, attempts++) * 1000)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if ((e as { code: number }).code === 8000013) {
|
|
215
|
+
// Looks like the JWT expired, fetch another one
|
|
216
|
+
jwt = await fetchJwt();
|
|
217
|
+
}
|
|
218
|
+
return getMissingHashes();
|
|
219
|
+
} else {
|
|
220
|
+
throw e;
|
|
221
|
+
}
|
|
204
222
|
}
|
|
205
|
-
|
|
223
|
+
};
|
|
224
|
+
const missingHashes = await getMissingHashes();
|
|
206
225
|
|
|
207
226
|
const sortedFiles = files
|
|
208
227
|
.filter((file) => missingHashes.includes(file.hash))
|
|
@@ -256,7 +275,7 @@ export const upload = async (
|
|
|
256
275
|
// Don't upload empty buckets (can happen for tiny projects)
|
|
257
276
|
if (bucket.files.length === 0) continue;
|
|
258
277
|
|
|
259
|
-
|
|
278
|
+
attempts = 0;
|
|
260
279
|
const doUpload = async (): Promise<void> => {
|
|
261
280
|
// Populate the payload only when actually uploading (this is limited to 3 concurrent uploads at 50 MiB per bucket meaning we'd only load in a max of ~150 MiB)
|
|
262
281
|
// This is so we don't run out of memory trying to upload the files.
|