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.
Files changed (55) hide show
  1. package/README.md +20 -2
  2. package/bin/wrangler.js +1 -1
  3. package/miniflare-dist/index.mjs +96 -34
  4. package/package.json +9 -4
  5. package/src/__tests__/configuration.test.ts +88 -16
  6. package/src/__tests__/dev.test.tsx +3 -0
  7. package/src/__tests__/generate.test.ts +93 -0
  8. package/src/__tests__/helpers/mock-cfetch.ts +54 -2
  9. package/src/__tests__/index.test.ts +10 -27
  10. package/src/__tests__/jest.setup.ts +31 -1
  11. package/src/__tests__/kv.test.ts +2 -2
  12. package/src/__tests__/metrics.test.ts +5 -0
  13. package/src/__tests__/publish.test.ts +497 -254
  14. package/src/__tests__/r2.test.ts +155 -71
  15. package/src/__tests__/user.test.ts +1 -0
  16. package/src/__tests__/validate-dev-props.test.ts +56 -0
  17. package/src/__tests__/whoami.test.tsx +60 -1
  18. package/src/bundle.ts +278 -44
  19. package/src/cfetch/internal.ts +34 -2
  20. package/src/config/config.ts +7 -2
  21. package/src/config/environment.ts +40 -8
  22. package/src/config/index.ts +13 -0
  23. package/src/config/validation.ts +101 -7
  24. package/src/create-worker-upload-form.ts +25 -0
  25. package/src/dev/dev.tsx +107 -28
  26. package/src/dev/local.tsx +20 -10
  27. package/src/dev/remote.tsx +39 -8
  28. package/src/dev/use-esbuild.ts +25 -0
  29. package/src/dev/validate-dev-props.ts +31 -0
  30. package/src/dev-registry.tsx +157 -0
  31. package/src/dev.tsx +93 -75
  32. package/src/generate.ts +112 -14
  33. package/src/index.tsx +182 -4
  34. package/src/inspect.ts +93 -5
  35. package/src/metrics/index.ts +1 -0
  36. package/src/metrics/metrics-dispatcher.ts +1 -0
  37. package/src/metrics/metrics-usage-headers.ts +24 -0
  38. package/src/metrics/send-event.ts +2 -2
  39. package/src/module-collection.ts +3 -3
  40. package/src/pages/constants.ts +1 -0
  41. package/src/pages/deployments.tsx +1 -1
  42. package/src/pages/dev.tsx +6 -1
  43. package/src/pages/publish.tsx +1 -1
  44. package/src/pages/upload.tsx +32 -13
  45. package/src/publish.ts +126 -112
  46. package/src/r2.ts +68 -0
  47. package/src/user/user.tsx +20 -2
  48. package/src/whoami.tsx +79 -1
  49. package/src/worker.ts +12 -0
  50. package/templates/first-party-worker-module-facade.ts +18 -0
  51. package/templates/format-dev-errors.ts +32 -0
  52. package/templates/{static-asset-facade.js → serve-static-assets.ts} +21 -7
  53. package/templates/service-bindings-module-facade.js +51 -0
  54. package/templates/service-bindings-sw-facade.js +39 -0
  55. 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 { createR2Bucket, deleteR2Bucket, listR2Buckets } from "./r2";
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
- logger.error(
261
- params.exceptionDetails.text,
262
- params.exceptionDetails.exception?.description ?? ""
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,
@@ -2,3 +2,4 @@ export { getMetricsDispatcher } from "./metrics-dispatcher";
2
2
  export type { Properties } from "./metrics-dispatcher";
3
3
  export { getMetricsConfig } from "./metrics-config";
4
4
  export * from "./send-event";
5
+ export { getMetricsUsageHeaders } from "./metrics-usage-headers";
@@ -67,6 +67,7 @@ export async function getMetricsDispatcher(options: MetricsConfigOptions) {
67
67
  properties: {
68
68
  category: "Workers",
69
69
  wranglerVersion,
70
+ os: process.platform + ":" + process.arch,
70
71
  ...event.properties,
71
72
  },
72
73
  });
@@ -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
- | "deploy pages project"
51
- | "list pages projects deployments"
50
+ | "create pages deployment"
51
+ | "list pages deployments"
52
52
  | "build pages functions"
53
53
  | "run dev"
54
54
  | "run pages dev";
@@ -123,9 +123,9 @@ export default function createModuleCollector(props: {
123
123
  {
124
124
  filter: new RegExp(
125
125
  "^(" +
126
- [...props.wrangler1xlegacyModuleReferences.fileNames].join(
127
- "|"
128
- ) +
126
+ [...props.wrangler1xlegacyModuleReferences.fileNames]
127
+ .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
128
+ .join("|") +
129
129
  ")$"
130
130
  ),
131
131
  },
@@ -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;
@@ -102,5 +102,5 @@ export async function ListHandler({
102
102
  patchConsole: false,
103
103
  });
104
104
  unmount();
105
- await metrics.sendMetricsEvent("list pages projects deployments");
105
+ await metrics.sendMetricsEvent("list pages deployments");
106
106
  }
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 === undefined) {
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,
@@ -334,5 +334,5 @@ export const Handler = async ({
334
334
  logger.log(
335
335
  `✨ Deployment complete! Take a peek over at ${deploymentResponse.url}`
336
336
  );
337
- await metrics.sendMetricsEvent("deploy pages project");
337
+ await metrics.sendMetricsEvent("create pages deployment");
338
338
  };
@@ -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
- const missingHashes = await fetchResult<string[]>(
194
- `/pages/assets/check-missing`,
195
- {
196
- method: "POST",
197
- headers: {
198
- "Content-Type": "application/json",
199
- Authorization: `Bearer ${jwt}`,
200
- },
201
- body: JSON.stringify({
202
- hashes: files.map(({ hash }) => hash),
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
- let attempts = 0;
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.