wrangler 2.0.22 → 2.0.25

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 (75) hide show
  1. package/README.md +20 -2
  2. package/bin/wrangler.js +1 -1
  3. package/miniflare-dist/index.mjs +643 -7
  4. package/package.json +17 -5
  5. package/src/__tests__/configuration.test.ts +89 -17
  6. package/src/__tests__/dev.test.tsx +121 -8
  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 +82 -61
  12. package/src/__tests__/metrics.test.ts +5 -0
  13. package/src/__tests__/publish.test.ts +573 -254
  14. package/src/__tests__/r2.test.ts +173 -71
  15. package/src/__tests__/tail.test.ts +93 -39
  16. package/src/__tests__/user.test.ts +1 -0
  17. package/src/__tests__/validate-dev-props.test.ts +56 -0
  18. package/src/__tests__/version.test.ts +35 -0
  19. package/src/__tests__/whoami.test.tsx +60 -1
  20. package/src/api/dev.ts +49 -9
  21. package/src/bundle.ts +298 -37
  22. package/src/cfetch/internal.ts +34 -2
  23. package/src/config/config.ts +15 -3
  24. package/src/config/environment.ts +40 -8
  25. package/src/config/index.ts +13 -0
  26. package/src/config/validation.ts +111 -9
  27. package/src/create-worker-preview.ts +3 -1
  28. package/src/create-worker-upload-form.ts +25 -0
  29. package/src/dev/dev.tsx +145 -31
  30. package/src/dev/local.tsx +116 -24
  31. package/src/dev/remote.tsx +39 -12
  32. package/src/dev/use-esbuild.ts +28 -0
  33. package/src/dev/validate-dev-props.ts +31 -0
  34. package/src/dev-registry.tsx +160 -0
  35. package/src/dev.tsx +148 -67
  36. package/src/generate.ts +112 -14
  37. package/src/index.tsx +252 -7
  38. package/src/inspect.ts +90 -5
  39. package/src/metrics/index.ts +1 -0
  40. package/src/metrics/metrics-dispatcher.ts +1 -0
  41. package/src/metrics/metrics-usage-headers.ts +24 -0
  42. package/src/metrics/send-event.ts +2 -2
  43. package/src/miniflare-cli/assets.ts +546 -0
  44. package/src/miniflare-cli/index.ts +157 -6
  45. package/src/module-collection.ts +3 -3
  46. package/src/pages/build.tsx +36 -28
  47. package/src/pages/constants.ts +4 -0
  48. package/src/pages/deployments.tsx +10 -10
  49. package/src/pages/dev.tsx +155 -651
  50. package/src/pages/functions/buildPlugin.ts +4 -0
  51. package/src/pages/functions/buildWorker.ts +4 -0
  52. package/src/pages/functions/routes-consolidation.test.ts +66 -0
  53. package/src/pages/functions/routes-consolidation.ts +29 -0
  54. package/src/pages/functions/routes-transformation.test.ts +271 -0
  55. package/src/pages/functions/routes-transformation.ts +125 -0
  56. package/src/pages/projects.tsx +9 -3
  57. package/src/pages/publish.tsx +57 -15
  58. package/src/pages/types.ts +9 -0
  59. package/src/pages/upload.tsx +38 -21
  60. package/src/publish.ts +139 -112
  61. package/src/r2.ts +81 -0
  62. package/src/tail/index.ts +15 -2
  63. package/src/tail/printing.ts +41 -3
  64. package/src/user/choose-account.tsx +20 -11
  65. package/src/user/user.tsx +20 -2
  66. package/src/whoami.tsx +79 -1
  67. package/src/worker.ts +12 -0
  68. package/templates/first-party-worker-module-facade.ts +18 -0
  69. package/templates/format-dev-errors.ts +32 -0
  70. package/templates/pages-shim.ts +9 -0
  71. package/templates/{static-asset-facade.js → serve-static-assets.ts} +21 -7
  72. package/templates/service-bindings-module-facade.js +51 -0
  73. package/templates/service-bindings-sw-facade.js +39 -0
  74. package/wrangler-dist/cli.d.ts +38 -3
  75. package/wrangler-dist/cli.js +45244 -25199
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,15 @@ 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
+ deleteR2Object,
55
+ getR2Object,
56
+ listR2Buckets,
57
+ putR2Object,
58
+ } from "./r2";
49
59
  import { getAssetPaths, getSiteAssetPaths } from "./sites";
50
60
  import {
51
61
  createTail,
@@ -67,6 +77,7 @@ import { workerNamespaceCommands } from "./worker-namespace";
67
77
  import type { Config } from "./config";
68
78
  import type { KeyValue } from "./kv";
69
79
  import type { TailCLIFilters } from "./tail";
80
+ import type { Readable } from "node:stream";
70
81
  import type { RawData } from "ws";
71
82
  import type { CommandModule } from "yargs";
72
83
  import type Yargs from "yargs";
@@ -76,6 +87,7 @@ export type ConfigPath = string | undefined;
76
87
  const resetColor = "\x1b[0m";
77
88
  const fgGreenColor = "\x1b[32m";
78
89
  export const DEFAULT_LOCAL_PORT = 8787;
90
+ export const DEFAULT_INSPECTOR_PORT = 9229;
79
91
 
80
92
  const proxy =
81
93
  process.env.https_proxy ||
@@ -258,11 +270,22 @@ function createCLIParser(argv: string[]) {
258
270
  ["*"],
259
271
  false,
260
272
  () => {},
261
- (args) => {
273
+ async (args) => {
262
274
  if (args._.length > 0) {
263
275
  throw new CommandLineArgsError(`Unknown command: ${args._}.`);
264
276
  } else {
265
- wrangler.showHelp("log");
277
+ // args.v will exist and be true in the case that no command is called, and the -v
278
+ // option is present. This is to allow for running asynchronous printWranglerBanner
279
+ // in the version command.
280
+ if (args.v) {
281
+ if (process.stdout.isTTY) {
282
+ await printWranglerBanner();
283
+ } else {
284
+ logger.log(wranglerVersion);
285
+ }
286
+ } else {
287
+ wrangler.showHelp("log");
288
+ }
266
289
  }
267
290
  }
268
291
  );
@@ -501,10 +524,16 @@ function createCLIParser(argv: string[]) {
501
524
  (args.config as ConfigPath) ||
502
525
  (args.script && findWranglerToml(path.dirname(args.script)));
503
526
  const config = readConfig(configPath, args);
504
- await metrics.sendMetricsEvent("deploy worker script", {
505
- sendMetrics: config.send_metrics,
506
- });
507
527
  const entry = await getEntry(args, config, "publish");
528
+ await metrics.sendMetricsEvent(
529
+ "deploy worker script",
530
+ {
531
+ usesTypeScript: /\.tsx?$/.test(entry.file),
532
+ },
533
+ {
534
+ sendMetrics: config.send_metrics,
535
+ }
536
+ );
508
537
 
509
538
  if (args.public) {
510
539
  throw new Error("The --public field has been renamed to --assets");
@@ -938,6 +967,7 @@ function createCLIParser(argv: string[]) {
938
967
  text_blobs: {},
939
968
  data_blobs: {},
940
969
  worker_namespaces: [],
970
+ logfwdr: { schema: undefined, bindings: [] },
941
971
  unsafe: [],
942
972
  },
943
973
  modules: [],
@@ -1220,7 +1250,9 @@ function createCLIParser(argv: string[]) {
1220
1250
 
1221
1251
  const accountId = await requireAuth(config);
1222
1252
 
1253
+ logger.log(`Deleting KV namespace ${id}.`);
1223
1254
  await deleteKVNamespace(accountId, id);
1255
+ logger.log(`Deleted KV namespace ${id}.`);
1224
1256
  await metrics.sendMetricsEvent("delete kv namespace", {
1225
1257
  sendMetrics: config.send_metrics,
1226
1258
  });
@@ -1700,6 +1732,7 @@ function createCLIParser(argv: string[]) {
1700
1732
  );
1701
1733
  }
1702
1734
  }
1735
+
1703
1736
  if (errors.length > 0) {
1704
1737
  throw new Error(
1705
1738
  `Unexpected JSON input from "${filename}".\n` +
@@ -1732,6 +1765,195 @@ function createCLIParser(argv: string[]) {
1732
1765
  wrangler.command("r2", "📦 Interact with an R2 store", (r2Yargs) => {
1733
1766
  return r2Yargs
1734
1767
  .command(subHelp)
1768
+ .command("object", "Manage R2 objects", (r2ObjectYargs) => {
1769
+ return r2ObjectYargs
1770
+ .command(
1771
+ "get <objectPath>",
1772
+ "Fetch an object from an R2 bucket",
1773
+ (Objectyargs) => {
1774
+ return Objectyargs.positional("objectPath", {
1775
+ describe:
1776
+ "The source object path in the form of {bucket}/{key}",
1777
+ type: "string",
1778
+ })
1779
+ .option("file", {
1780
+ describe: "The destination file to create",
1781
+ alias: "f",
1782
+ conflicts: "pipe",
1783
+ requiresArg: true,
1784
+ type: "string",
1785
+ })
1786
+ .option("pipe", {
1787
+ describe:
1788
+ "Enables the file to be piped to a destination, rather than specified with the --file option",
1789
+ alias: "p",
1790
+ conflicts: "file",
1791
+ type: "boolean",
1792
+ });
1793
+ },
1794
+ async (objectGetYargs) => {
1795
+ const config = readConfig(
1796
+ objectGetYargs.config as ConfigPath,
1797
+ objectGetYargs
1798
+ );
1799
+ const accountId = await requireAuth(config);
1800
+ const { objectPath, pipe } = objectGetYargs;
1801
+ const { bucket, key } = bucketAndKeyFromObjectPath(objectPath);
1802
+
1803
+ let file = objectGetYargs.file;
1804
+ if (!file && !pipe) {
1805
+ file = key;
1806
+ }
1807
+ if (!pipe) {
1808
+ await printWranglerBanner();
1809
+ logger.log(`Downloading "${key}" from "${bucket}".`);
1810
+ }
1811
+ const input = await getR2Object(accountId, bucket, key);
1812
+ const output = file ? fs.createWriteStream(file) : process.stdout;
1813
+ await new Promise<void>((resolve, reject) => {
1814
+ stream.pipeline(input, output, (err: unknown) => {
1815
+ err ? reject(err) : resolve();
1816
+ });
1817
+ });
1818
+ if (!pipe) logger.log("Download complete.");
1819
+ }
1820
+ )
1821
+ .command(
1822
+ "put <objectPath>",
1823
+ "Create an object in an R2 bucket",
1824
+ (Objectyargs) => {
1825
+ return Objectyargs.positional("objectPath", {
1826
+ describe:
1827
+ "The destination object path in the form of {bucket}/{key}",
1828
+ type: "string",
1829
+ })
1830
+ .option("file", {
1831
+ describe: "The path of the file to upload",
1832
+ alias: "f",
1833
+ conflicts: "pipe",
1834
+ requiresArg: true,
1835
+ type: "string",
1836
+ })
1837
+ .option("pipe", {
1838
+ describe:
1839
+ "Enables the file to be piped in, rather than specified with the --file option",
1840
+ alias: "p",
1841
+ conflicts: "file",
1842
+ type: "boolean",
1843
+ })
1844
+ .option("content-type", {
1845
+ describe:
1846
+ "A standard MIME type describing the format of the object data",
1847
+ alias: "ct",
1848
+ requiresArg: true,
1849
+ type: "string",
1850
+ })
1851
+ .option("content-disposition", {
1852
+ describe:
1853
+ "Specifies presentational information for the object",
1854
+ alias: "cd",
1855
+ requiresArg: true,
1856
+ type: "string",
1857
+ })
1858
+ .option("content-encoding", {
1859
+ describe:
1860
+ "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",
1861
+ alias: "ce",
1862
+ requiresArg: true,
1863
+ type: "string",
1864
+ })
1865
+ .option("content-language", {
1866
+ describe: "The language the content is in",
1867
+ alias: "cl",
1868
+ requiresArg: true,
1869
+ type: "string",
1870
+ })
1871
+ .option("cache-control", {
1872
+ describe:
1873
+ "Specifies caching behavior along the request/reply chain",
1874
+ alias: "cc",
1875
+ requiresArg: true,
1876
+ type: "string",
1877
+ })
1878
+ .option("expires", {
1879
+ describe:
1880
+ "The date and time at which the object is no longer cacheable",
1881
+ alias: "e",
1882
+ requiresArg: true,
1883
+ type: "string",
1884
+ });
1885
+ },
1886
+ async (objectPutYargs) => {
1887
+ await printWranglerBanner();
1888
+
1889
+ const config = readConfig(
1890
+ objectPutYargs.config as ConfigPath,
1891
+ objectPutYargs
1892
+ );
1893
+ const accountId = await requireAuth(config);
1894
+ const { objectPath, file, pipe, ...options } = objectPutYargs;
1895
+ const { bucket, key } = bucketAndKeyFromObjectPath(objectPath);
1896
+ if (!file && !pipe) {
1897
+ throw new CommandLineArgsError(
1898
+ "Either the --file or --pipe options are required."
1899
+ );
1900
+ }
1901
+ let object: Readable | Buffer;
1902
+ let objectSize: number;
1903
+ if (file) {
1904
+ object = fs.createReadStream(file);
1905
+ const stats = fs.statSync(file);
1906
+ objectSize = stats.size;
1907
+ } else {
1908
+ object = await new Promise<Buffer>((resolve, reject) => {
1909
+ const stdin = process.stdin;
1910
+ const chunks = Array<Buffer>();
1911
+ stdin.on("data", (chunk) => chunks.push(chunk));
1912
+ stdin.on("end", () => resolve(Buffer.concat(chunks)));
1913
+ stdin.on("error", (err) =>
1914
+ reject(
1915
+ new CommandLineArgsError(
1916
+ `Could not pipe. Reason: "${err.message}"`
1917
+ )
1918
+ )
1919
+ );
1920
+ });
1921
+ objectSize = object.byteLength;
1922
+ }
1923
+
1924
+ logger.log(`Creating object "${key}" in bucket "${bucket}".`);
1925
+ await putR2Object(accountId, bucket, key, object, {
1926
+ ...options,
1927
+ "content-length": `${objectSize}`,
1928
+ });
1929
+ logger.log("Upload complete.");
1930
+ }
1931
+ )
1932
+ .command(
1933
+ "delete <objectPath>",
1934
+ "Delete an object in an R2 bucket",
1935
+ (objectDeleteYargs) => {
1936
+ return objectDeleteYargs.positional("objectPath", {
1937
+ describe:
1938
+ "The destination object path in the form of {bucket}/{key}",
1939
+ type: "string",
1940
+ });
1941
+ },
1942
+ async (args) => {
1943
+ const { objectPath } = args;
1944
+ await printWranglerBanner();
1945
+
1946
+ const config = readConfig(args.config as ConfigPath, args);
1947
+ const accountId = await requireAuth(config);
1948
+ const { bucket, key } = bucketAndKeyFromObjectPath(objectPath);
1949
+ logger.log(`Deleting object "${key}" from bucket "${bucket}".`);
1950
+
1951
+ await deleteR2Object(accountId, bucket, key);
1952
+ logger.log("Delete complete.");
1953
+ }
1954
+ );
1955
+ })
1956
+
1735
1957
  .command("bucket", "Manage R2 buckets", (r2BucketYargs) => {
1736
1958
  r2BucketYargs.command(
1737
1959
  "create <name>",
@@ -1903,6 +2125,29 @@ function createCLIParser(argv: string[]) {
1903
2125
  }
1904
2126
  );
1905
2127
 
2128
+ // This set to false to allow overwrite of default behaviour
2129
+ wrangler.version(false);
2130
+
2131
+ // version
2132
+ wrangler.command(
2133
+ "version",
2134
+ false,
2135
+ () => {},
2136
+ async () => {
2137
+ if (process.stdout.isTTY) {
2138
+ await printWranglerBanner();
2139
+ } else {
2140
+ logger.log(wranglerVersion);
2141
+ }
2142
+ }
2143
+ );
2144
+
2145
+ wrangler.option("v", {
2146
+ describe: "Show version number",
2147
+ alias: "version",
2148
+ type: "boolean",
2149
+ });
2150
+
1906
2151
  wrangler.option("config", {
1907
2152
  alias: "c",
1908
2153
  describe: "Path to .toml configuration file",
@@ -1912,7 +2157,7 @@ function createCLIParser(argv: string[]) {
1912
2157
 
1913
2158
  wrangler.group(["config", "help", "version"], "Flags:");
1914
2159
  wrangler.help().alias("h", "help");
1915
- wrangler.version(wranglerVersion).alias("v", "version");
2160
+
1916
2161
  wrangler.exitProcess(false);
1917
2162
 
1918
2163
  return wrangler;
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,93 @@ 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}:${pos.line}:${pos.column})`
324
+ );
325
+ }
326
+ }
327
+ } catch {
328
+ // Line failed to parse through the sourcemap consumer
329
+ // We should handle this better
330
+ }
331
+ }
332
+ );
333
+ }
334
+ );
335
+
336
+ // Log the parsed stacktrace
337
+ logger.error(
338
+ params.exceptionDetails.text,
339
+ exceptionLines.join("\n")
340
+ );
341
+ } else {
342
+ // We log the stacktrace to the terminal
343
+ logger.error(
344
+ params.exceptionDetails.text,
345
+ params.exceptionDetails.exception?.description ?? ""
346
+ );
347
+ }
264
348
  }
265
349
  if (evt.method === "Runtime.consoleAPICalled") {
266
350
  logConsoleMessage(
@@ -333,6 +417,7 @@ export default function useInspector(props: InspectorProps) {
333
417
  }, [
334
418
  props.inspectorUrl,
335
419
  props.logToTerminal,
420
+ props.sourceMapPath,
336
421
  wsServer,
337
422
  // We use a state value as a sigil to trigger a retry of the
338
423
  // 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";