wrangler 2.0.2 → 2.0.6

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/src/index.tsx CHANGED
@@ -22,17 +22,19 @@ import { confirm, prompt } from "./dialogs";
22
22
  import { getEntry } from "./entry";
23
23
  import { DeprecationError } from "./errors";
24
24
  import {
25
- getNamespaceId,
26
- listNamespaces,
27
- listNamespaceKeys,
28
- putKeyValue,
29
- putBulkKeyValue,
30
- deleteBulkKeyValue,
31
- createNamespace,
32
- isValidNamespaceBinding,
33
- getKeyValue,
34
- isKeyValue,
35
- unexpectedKeyValueProps,
25
+ getKVNamespaceId,
26
+ listKVNamespaces,
27
+ listKVNamespaceKeys,
28
+ putKVKeyValue,
29
+ putKVBulkKeyValue,
30
+ deleteKVBulkKeyValue,
31
+ createKVNamespace,
32
+ isValidKVNamespaceBinding,
33
+ getKVKeyValue,
34
+ isKVKeyValue,
35
+ unexpectedKVKeyValueProps,
36
+ deleteKVNamespace,
37
+ deleteKVKeyValue,
36
38
  } from "./kv";
37
39
  import { logger } from "./logger";
38
40
  import { getPackageManager } from "./package-manager";
@@ -73,6 +75,7 @@ type ConfigPath = string | undefined;
73
75
 
74
76
  const resetColor = "\x1b[0m";
75
77
  const fgGreenColor = "\x1b[32m";
78
+ const DEFAULT_LOCAL_PORT = 8787;
76
79
 
77
80
  function getRules(config: Config): Config["rules"] {
78
81
  const rules = config.rules ?? config.build?.upload?.rules ?? [];
@@ -203,9 +206,17 @@ function demandOneOfOption(...options: string[]) {
203
206
  };
204
207
  }
205
208
 
209
+ /**
210
+ * Remove trailing white space from inputs.
211
+ * Matching Wrangler legacy behavior with handling inputs
212
+ */
213
+ function trimTrailingWhitespace(str: string) {
214
+ return str.trimEnd();
215
+ }
216
+
206
217
  class CommandLineArgsError extends Error {}
207
218
 
208
- export async function main(argv: string[]): Promise<void> {
219
+ function createCLIParser(argv: string[]) {
209
220
  const wrangler = makeCLI(argv)
210
221
  .strict()
211
222
  // We handle errors ourselves in a try-catch around `yargs.parse`.
@@ -406,7 +417,12 @@ export async function main(argv: string[]): Promise<void> {
406
417
  const isInsideGitProject = Boolean(
407
418
  await findUp(".git", { cwd: creationDirectory, type: "directory" })
408
419
  );
409
- const isGitInstalled = (await execa("git", ["--version"])).exitCode === 0;
420
+ let isGitInstalled;
421
+ try {
422
+ isGitInstalled = (await execa("git", ["--version"])).exitCode === 0;
423
+ } catch (err) {
424
+ isGitInstalled = false;
425
+ }
410
426
  if (!isInsideGitProject && isGitInstalled) {
411
427
  const shouldInitGit =
412
428
  yesFlag ||
@@ -712,7 +728,7 @@ export async function main(argv: string[]): Promise<void> {
712
728
  () => {
713
729
  // "[DEPRECATED] 🦀 Build your project (if applicable)",
714
730
  throw new DeprecationError(
715
- "`wrangler build` has been deprecated, please refer to https://github.com/cloudflare/wrangler2/blob/main/docs/deprecations.md#build for alternatives"
731
+ "`wrangler build` has been deprecated, please refer to https://developers.cloudflare.com/workers/wrangler/migration/deprecations/#build for alternatives"
716
732
  );
717
733
  }
718
734
  );
@@ -725,7 +741,7 @@ export async function main(argv: string[]): Promise<void> {
725
741
  () => {
726
742
  // "🕵️ Authenticate Wrangler with a Cloudflare API Token",
727
743
  throw new DeprecationError(
728
- "`wrangler config` has been deprecated, please refer to https://github.com/cloudflare/wrangler2/blob/main/docs/deprecations.md#config for alternatives"
744
+ "`wrangler config` has been deprecated, please refer to https://developers.cloudflare.com/workers/wrangler/migration/deprecations/#config for alternatives"
729
745
  );
730
746
  }
731
747
  );
@@ -869,6 +885,11 @@ export async function main(argv: string[]): Promise<void> {
869
885
  describe: "Enable dev tools",
870
886
  type: "boolean",
871
887
  deprecated: true,
888
+ })
889
+ .option("legacy-env", {
890
+ type: "boolean",
891
+ describe: "Use legacy environments",
892
+ hidden: true,
872
893
  });
873
894
  },
874
895
  async (args) => {
@@ -879,6 +900,19 @@ export async function main(argv: string[]): Promise<void> {
879
900
  const config = readConfig(configPath, args);
880
901
  const entry = await getEntry(args, config, "dev");
881
902
 
903
+ if (config.services && config.services.length > 0) {
904
+ logger.warn(
905
+ `This worker is bound to live services: ${config.services
906
+ .map(
907
+ (service) =>
908
+ `${service.binding} (${service.service}${
909
+ service.environment ? `@${service.environment}` : ""
910
+ })`
911
+ )
912
+ .join(", ")}`
913
+ );
914
+ }
915
+
882
916
  if (args.inspect) {
883
917
  logger.warn(
884
918
  "Passing --inspect is unnecessary, now you can always connect to devtools."
@@ -916,6 +950,9 @@ export async function main(argv: string[]): Promise<void> {
916
950
  * try to extract a host from it
917
951
  */
918
952
  function getHost(urlLike: string): string | undefined {
953
+ // strip leading * / *.
954
+ urlLike = urlLike.replace(/^\*(\.)?/g, "");
955
+
919
956
  if (
920
957
  !(urlLike.startsWith("http://") || urlLike.startsWith("https://"))
921
958
  ) {
@@ -995,7 +1032,7 @@ export async function main(argv: string[]): Promise<void> {
995
1032
  const nodeCompat = args.nodeCompat ?? config.node_compat;
996
1033
  if (nodeCompat) {
997
1034
  logger.warn(
998
- "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."
1035
+ "Enabling node.js compatibility mode for built-ins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
999
1036
  );
1000
1037
  }
1001
1038
 
@@ -1027,7 +1064,9 @@ export async function main(argv: string[]): Promise<void> {
1027
1064
  args.siteExclude
1028
1065
  )}
1029
1066
  port={
1030
- args.port || config.dev?.port || (await getPort({ port: 8787 }))
1067
+ args.port ||
1068
+ config.dev.port ||
1069
+ (await getPort({ port: DEFAULT_LOCAL_PORT }))
1031
1070
  }
1032
1071
  ip={args.ip || config.dev.ip}
1033
1072
  inspectorPort={
@@ -1084,6 +1123,7 @@ export async function main(argv: string[]): Promise<void> {
1084
1123
  };
1085
1124
  }
1086
1125
  ),
1126
+ services: config.services,
1087
1127
  unsafe: config.unsafe?.bindings,
1088
1128
  }}
1089
1129
  crons={config.triggers.crons}
@@ -1206,6 +1246,11 @@ export async function main(argv: string[]): Promise<void> {
1206
1246
  .option("dry-run", {
1207
1247
  describe: "Don't actually publish",
1208
1248
  type: "boolean",
1249
+ })
1250
+ .option("legacy-env", {
1251
+ type: "boolean",
1252
+ describe: "Use legacy environments",
1253
+ hidden: true,
1209
1254
  });
1210
1255
  },
1211
1256
  async (args) => {
@@ -1233,7 +1278,7 @@ export async function main(argv: string[]): Promise<void> {
1233
1278
  );
1234
1279
  }
1235
1280
 
1236
- const accountId = await requireAuth(config);
1281
+ const accountId = args.dryRun ? undefined : await requireAuth(config);
1237
1282
 
1238
1283
  const assetPaths = getAssetPaths(
1239
1284
  config,
@@ -1327,6 +1372,11 @@ export async function main(argv: string[]): Promise<void> {
1327
1372
  default: false,
1328
1373
  describe:
1329
1374
  "If a log would have been filtered out, send it through anyway alongside the filter which would have blocked it.",
1375
+ })
1376
+ .option("legacy-env", {
1377
+ type: "boolean",
1378
+ describe: "Use legacy environments",
1379
+ hidden: true,
1330
1380
  });
1331
1381
  },
1332
1382
  async (args) => {
@@ -1476,7 +1526,9 @@ export async function main(argv: string[]): Promise<void> {
1476
1526
  enableLocalPersistence={false}
1477
1527
  accountId={accountId}
1478
1528
  assetPaths={undefined}
1479
- port={config.dev?.port}
1529
+ port={
1530
+ config.dev.port || (await getPort({ port: DEFAULT_LOCAL_PORT }))
1531
+ }
1480
1532
  ip={config.dev.ip}
1481
1533
  public={undefined}
1482
1534
  compatibilityDate={getDevCompatibilityDate(config)}
@@ -1524,6 +1576,7 @@ export async function main(argv: string[]): Promise<void> {
1524
1576
  };
1525
1577
  }
1526
1578
  ),
1579
+ services: config.services,
1527
1580
  unsafe: config.unsafe?.bindings,
1528
1581
  }}
1529
1582
  crons={config.triggers.crons}
@@ -1623,7 +1676,7 @@ export async function main(argv: string[]): Promise<void> {
1623
1676
  },
1624
1677
  () => {
1625
1678
  throw new DeprecationError(
1626
- "`wrangler subdomain` has been deprecated, please refer to https://github.com/cloudflare/wrangler2/blob/main/docs/deprecations.md#subdomain for alternatives"
1679
+ "`wrangler subdomain` has been deprecated, please refer to https://developers.cloudflare.com/workers/wrangler/migration/deprecations/#subdomain for alternatives"
1627
1680
  );
1628
1681
  }
1629
1682
  );
@@ -1635,6 +1688,11 @@ export async function main(argv: string[]): Promise<void> {
1635
1688
  (secretYargs) => {
1636
1689
  return secretYargs
1637
1690
  .command(subHelp)
1691
+ .option("legacy-env", {
1692
+ type: "boolean",
1693
+ describe: "Use legacy environments",
1694
+ hidden: true,
1695
+ })
1638
1696
  .command(
1639
1697
  "put <key>",
1640
1698
  "Create or update a secret variable for a script",
@@ -1669,9 +1727,11 @@ export async function main(argv: string[]): Promise<void> {
1669
1727
  const accountId = await requireAuth(config);
1670
1728
 
1671
1729
  const isInteractive = process.stdin.isTTY;
1672
- const secretValue = isInteractive
1673
- ? await prompt("Enter a secret value:", "password")
1674
- : await readFromStdin();
1730
+ const secretValue = trimTrailingWhitespace(
1731
+ isInteractive
1732
+ ? await prompt("Enter a secret value:", "password")
1733
+ : await readFromStdin()
1734
+ );
1675
1735
 
1676
1736
  logger.log(
1677
1737
  `🌀 Creating the secret for script ${scriptName} ${
@@ -1716,6 +1776,7 @@ export async function main(argv: string[]): Promise<void> {
1716
1776
  vars: {},
1717
1777
  durable_objects: { bindings: [] },
1718
1778
  r2_buckets: [],
1779
+ services: [],
1719
1780
  wasm_modules: {},
1720
1781
  text_blobs: {},
1721
1782
  data_blobs: {},
@@ -1884,7 +1945,7 @@ export async function main(argv: string[]): Promise<void> {
1884
1945
  async (args) => {
1885
1946
  await printWranglerBanner();
1886
1947
 
1887
- if (!isValidNamespaceBinding(args.namespace)) {
1948
+ if (!isValidKVNamespaceBinding(args.namespace)) {
1888
1949
  throw new CommandLineArgsError(
1889
1950
  `The namespace binding name "${args.namespace}" is invalid. It can only have alphanumeric and _ characters, and cannot begin with a number.`
1890
1951
  );
@@ -1907,7 +1968,7 @@ export async function main(argv: string[]): Promise<void> {
1907
1968
  // TODO: generate a binding name stripping non alphanumeric chars
1908
1969
 
1909
1970
  logger.log(`🌀 Creating namespace with title "${title}"`);
1910
- const namespaceId = await createNamespace(accountId, title);
1971
+ const namespaceId = await createKVNamespace(accountId, title);
1911
1972
 
1912
1973
  logger.log("✨ Success!");
1913
1974
  const envString = args.env ? ` under [env.${args.env}]` : "";
@@ -1934,7 +1995,7 @@ export async function main(argv: string[]): Promise<void> {
1934
1995
  // TODO: we should show bindings if they exist for given ids
1935
1996
 
1936
1997
  logger.log(
1937
- JSON.stringify(await listNamespaces(accountId), null, " ")
1998
+ JSON.stringify(await listKVNamespaces(accountId), null, " ")
1938
1999
  );
1939
2000
  }
1940
2001
  )
@@ -1971,7 +2032,7 @@ export async function main(argv: string[]): Promise<void> {
1971
2032
 
1972
2033
  let id;
1973
2034
  try {
1974
- id = getNamespaceId(args, config);
2035
+ id = getKVNamespaceId(args, config);
1975
2036
  } catch (e) {
1976
2037
  throw new CommandLineArgsError(
1977
2038
  "Not able to delete namespace.\n" + ((e as Error).message ?? e)
@@ -1980,10 +2041,7 @@ export async function main(argv: string[]): Promise<void> {
1980
2041
 
1981
2042
  const accountId = await requireAuth(config);
1982
2043
 
1983
- await fetchResult<{ id: string }>(
1984
- `/accounts/${accountId}/storage/kv/namespaces/${id}`,
1985
- { method: "DELETE" }
1986
- );
2044
+ await deleteKVNamespace(accountId, id);
1987
2045
 
1988
2046
  // TODO: recommend they remove it from wrangler.toml
1989
2047
 
@@ -2068,7 +2126,7 @@ export async function main(argv: string[]): Promise<void> {
2068
2126
  async ({ key, ttl, expiration, ...args }) => {
2069
2127
  await printWranglerBanner();
2070
2128
  const config = readConfig(args.config as ConfigPath, args);
2071
- const namespaceId = getNamespaceId(args, config);
2129
+ const namespaceId = getKVNamespaceId(args, config);
2072
2130
  // One of `args.path` and `args.value` must be defined
2073
2131
  const value = args.path
2074
2132
  ? readFileSync(args.path)
@@ -2087,7 +2145,7 @@ export async function main(argv: string[]): Promise<void> {
2087
2145
 
2088
2146
  const accountId = await requireAuth(config);
2089
2147
 
2090
- await putKeyValue(accountId, namespaceId, {
2148
+ await putKVKeyValue(accountId, namespaceId, {
2091
2149
  key,
2092
2150
  value,
2093
2151
  expiration,
@@ -2132,11 +2190,11 @@ export async function main(argv: string[]): Promise<void> {
2132
2190
  async ({ prefix, ...args }) => {
2133
2191
  // TODO: support for limit+cursor (pagination)
2134
2192
  const config = readConfig(args.config as ConfigPath, args);
2135
- const namespaceId = getNamespaceId(args, config);
2193
+ const namespaceId = getKVNamespaceId(args, config);
2136
2194
 
2137
2195
  const accountId = await requireAuth(config);
2138
2196
 
2139
- const results = await listNamespaceKeys(
2197
+ const results = await listKVNamespaceKeys(
2140
2198
  accountId,
2141
2199
  namespaceId,
2142
2200
  prefix
@@ -2184,11 +2242,11 @@ export async function main(argv: string[]): Promise<void> {
2184
2242
  },
2185
2243
  async ({ key, ...args }) => {
2186
2244
  const config = readConfig(args.config as ConfigPath, args);
2187
- const namespaceId = getNamespaceId(args, config);
2245
+ const namespaceId = getKVNamespaceId(args, config);
2188
2246
 
2189
2247
  const accountId = await requireAuth(config);
2190
2248
 
2191
- logger.log(await getKeyValue(accountId, namespaceId, key));
2249
+ logger.log(await getKVKeyValue(accountId, namespaceId, key));
2192
2250
  }
2193
2251
  )
2194
2252
  .command(
@@ -2226,7 +2284,7 @@ export async function main(argv: string[]): Promise<void> {
2226
2284
  async ({ key, ...args }) => {
2227
2285
  await printWranglerBanner();
2228
2286
  const config = readConfig(args.config as ConfigPath, args);
2229
- const namespaceId = getNamespaceId(args, config);
2287
+ const namespaceId = getKVNamespaceId(args, config);
2230
2288
 
2231
2289
  logger.log(
2232
2290
  `Deleting the key "${key}" on namespace ${namespaceId}.`
@@ -2234,10 +2292,7 @@ export async function main(argv: string[]): Promise<void> {
2234
2292
 
2235
2293
  const accountId = await requireAuth(config);
2236
2294
 
2237
- await fetchResult(
2238
- `/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`,
2239
- { method: "DELETE" }
2240
- );
2295
+ await deleteKVKeyValue(accountId, namespaceId, key);
2241
2296
  }
2242
2297
  );
2243
2298
  }
@@ -2289,7 +2344,7 @@ export async function main(argv: string[]): Promise<void> {
2289
2344
  // but we'll do that in the future if needed.
2290
2345
 
2291
2346
  const config = readConfig(args.config as ConfigPath, args);
2292
- const namespaceId = getNamespaceId(args, config);
2347
+ const namespaceId = getKVNamespaceId(args, config);
2293
2348
  const content = parseJSON(readFileSync(filename), filename);
2294
2349
 
2295
2350
  if (!Array.isArray(content)) {
@@ -2303,18 +2358,12 @@ export async function main(argv: string[]): Promise<void> {
2303
2358
  const warnings: string[] = [];
2304
2359
  for (let i = 0; i < content.length; i++) {
2305
2360
  const keyValue = content[i];
2306
- if (typeof keyValue !== "object") {
2307
- errors.push(
2308
- `The item at index ${i} is type: "${typeof keyValue}" - ${JSON.stringify(
2309
- keyValue
2310
- )}`
2311
- );
2312
- } else if (!isKeyValue(keyValue)) {
2361
+ if (!isKVKeyValue(keyValue)) {
2313
2362
  errors.push(
2314
2363
  `The item at index ${i} is ${JSON.stringify(keyValue)}`
2315
2364
  );
2316
2365
  } else {
2317
- const props = unexpectedKeyValueProps(keyValue);
2366
+ const props = unexpectedKVKeyValueProps(keyValue);
2318
2367
  if (props.length > 0) {
2319
2368
  warnings.push(
2320
2369
  `The item at index ${i} contains unexpected properties: ${JSON.stringify(
@@ -2347,7 +2396,7 @@ export async function main(argv: string[]): Promise<void> {
2347
2396
  }
2348
2397
 
2349
2398
  const accountId = await requireAuth(config);
2350
- await putBulkKeyValue(
2399
+ await putKVBulkKeyValue(
2351
2400
  accountId,
2352
2401
  namespaceId,
2353
2402
  content,
@@ -2399,7 +2448,7 @@ export async function main(argv: string[]): Promise<void> {
2399
2448
  async ({ filename, ...args }) => {
2400
2449
  await printWranglerBanner();
2401
2450
  const config = readConfig(args.config as ConfigPath, args);
2402
- const namespaceId = getNamespaceId(args, config);
2451
+ const namespaceId = getKVNamespaceId(args, config);
2403
2452
 
2404
2453
  if (!args.force) {
2405
2454
  const result = await confirm(
@@ -2444,7 +2493,7 @@ export async function main(argv: string[]): Promise<void> {
2444
2493
 
2445
2494
  const accountId = await requireAuth(config);
2446
2495
 
2447
- await deleteBulkKeyValue(
2496
+ await deleteKVBulkKeyValue(
2448
2497
  accountId,
2449
2498
  namespaceId,
2450
2499
  content,
@@ -2604,31 +2653,33 @@ export async function main(argv: string[]): Promise<void> {
2604
2653
  }
2605
2654
  );
2606
2655
 
2607
- wrangler
2608
- .option("legacy-env", {
2609
- type: "boolean",
2610
- describe: "Use legacy environments",
2611
- })
2612
- .option("config", {
2613
- alias: "c",
2614
- describe: "Path to .toml configuration file",
2615
- type: "string",
2616
- requiresArg: true,
2617
- });
2656
+ wrangler.option("config", {
2657
+ alias: "c",
2658
+ describe: "Path to .toml configuration file",
2659
+ type: "string",
2660
+ requiresArg: true,
2661
+ });
2618
2662
 
2619
- wrangler.group(["config", "help", "version", "legacy-env"], "Flags:");
2663
+ wrangler.group(["config", "help", "version"], "Flags:");
2620
2664
  wrangler.help().alias("h", "help");
2621
2665
  wrangler.version(wranglerVersion).alias("v", "version");
2622
2666
  wrangler.exitProcess(false);
2623
2667
 
2668
+ return wrangler;
2669
+ }
2670
+
2671
+ export async function main(argv: string[]): Promise<void> {
2672
+ const wrangler = createCLIParser(argv);
2624
2673
  try {
2625
2674
  await wrangler.parse();
2626
2675
  } catch (e) {
2627
2676
  logger.log(""); // Just adds a bit of space
2628
2677
  if (e instanceof CommandLineArgsError) {
2629
- wrangler.showHelp("error");
2630
- logger.log(""); // Add a bit of space.
2631
2678
  logger.error(e.message);
2679
+ // We are not able to ask the `wrangler` CLI parser to show help for a subcommand programmatically.
2680
+ // The workaround is to re-run the parsing with an additional `--help` flag, which will result in the correct help message being displayed.
2681
+ // The `wrangler` object is "frozen"; we cannot reuse that with different args, so we must create a new CLI parser to generate the help message.
2682
+ await createCLIParser([...argv, "--help"]).parse();
2632
2683
  } else if (e instanceof ParseError) {
2633
2684
  e.notes.push({
2634
2685
  text: "\nIf you think this is a bug, please open an issue at: https://github.com/cloudflare/wrangler2/issues/new",
package/src/kv.ts CHANGED
@@ -19,7 +19,7 @@ type KvArgs = {
19
19
  *
20
20
  * @returns the generated id of the created namespace.
21
21
  */
22
- export async function createNamespace(
22
+ export async function createKVNamespace(
23
23
  accountId: string,
24
24
  title: string
25
25
  ): Promise<string> {
@@ -51,7 +51,7 @@ export interface KVNamespaceInfo {
51
51
  /**
52
52
  * Fetch a list of all the namespaces under the given `accountId`.
53
53
  */
54
- export async function listNamespaces(
54
+ export async function listKVNamespaces(
55
55
  accountId: string
56
56
  ): Promise<KVNamespaceInfo[]> {
57
57
  const pageSize = 100;
@@ -83,7 +83,7 @@ export interface NamespaceKeyInfo {
83
83
  metadata?: { [key: string]: unknown };
84
84
  }
85
85
 
86
- export async function listNamespaceKeys(
86
+ export async function listKVNamespaceKeys(
87
87
  accountId: string,
88
88
  namespaceId: string,
89
89
  prefix = ""
@@ -95,6 +95,16 @@ export async function listNamespaceKeys(
95
95
  );
96
96
  }
97
97
 
98
+ export async function deleteKVNamespace(
99
+ accountId: string,
100
+ namespaceId: string
101
+ ) {
102
+ return await fetchResult<{ id: string }>(
103
+ `/accounts/${accountId}/storage/kv/namespaces/${namespaceId}`,
104
+ { method: "DELETE" }
105
+ );
106
+ }
107
+
98
108
  /**
99
109
  * Information about a key-value pair, including its "metadata" fields.
100
110
  */
@@ -119,9 +129,24 @@ const KeyValueKeys = new Set([
119
129
  /**
120
130
  * Is the given object a valid `KeyValue` type?
121
131
  */
122
- export function isKeyValue(keyValue: object): keyValue is KeyValue {
123
- const props = Object.keys(keyValue);
124
- if (!props.includes("key") || !props.includes("value")) {
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ export function isKVKeyValue(keyValue: any): keyValue is KeyValue {
134
+ // (keyValue could indeed be any-thing)
135
+ if (
136
+ typeof keyValue !== "object" ||
137
+ typeof keyValue.key !== "string" ||
138
+ typeof keyValue.value !== "string" ||
139
+ !(
140
+ keyValue.expiration === undefined ||
141
+ typeof keyValue.expiration === "number"
142
+ ) ||
143
+ !(
144
+ keyValue.expiration_ttl === undefined ||
145
+ typeof keyValue.expiration_ttl === "number"
146
+ ) ||
147
+ !(keyValue.base64 === undefined || typeof keyValue.base64 === "boolean") ||
148
+ !(keyValue.metadata === undefined || typeof keyValue.metadata === "object")
149
+ ) {
125
150
  return false;
126
151
  }
127
152
  return true;
@@ -130,12 +155,12 @@ export function isKeyValue(keyValue: object): keyValue is KeyValue {
130
155
  /**
131
156
  * Get all the properties on the `keyValue` that are not expected.
132
157
  */
133
- export function unexpectedKeyValueProps(keyValue: KeyValue): string[] {
158
+ export function unexpectedKVKeyValueProps(keyValue: KeyValue): string[] {
134
159
  const props = Object.keys(keyValue);
135
160
  return props.filter((prop) => !KeyValueKeys.has(prop));
136
161
  }
137
162
 
138
- export async function putKeyValue(
163
+ export async function putKVKeyValue(
139
164
  accountId: string,
140
165
  namespaceId: string,
141
166
  keyValue: KeyValue
@@ -151,21 +176,36 @@ export async function putKeyValue(
151
176
  }
152
177
  }
153
178
  return await fetchResult(
154
- `/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${keyValue.key}`,
179
+ `/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${encodeURIComponent(
180
+ keyValue.key
181
+ )}`,
155
182
  { method: "PUT", body: keyValue.value },
156
183
  searchParams
157
184
  );
158
185
  }
159
186
 
160
- export async function getKeyValue(
187
+ export async function getKVKeyValue(
161
188
  accountId: string,
162
189
  namespaceId: string,
163
190
  key: string
164
191
  ): Promise<string> {
165
- return await fetchKVGetValue(accountId, namespaceId, key);
192
+ return await fetchKVGetValue(accountId, namespaceId, encodeURIComponent(key));
193
+ }
194
+
195
+ export async function deleteKVKeyValue(
196
+ accountId: string,
197
+ namespaceId: string,
198
+ key: string
199
+ ) {
200
+ return await fetchResult(
201
+ `/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${encodeURIComponent(
202
+ key
203
+ )}`,
204
+ { method: "DELETE" }
205
+ );
166
206
  }
167
207
 
168
- export async function putBulkKeyValue(
208
+ export async function putKVBulkKeyValue(
169
209
  accountId: string,
170
210
  namespaceId: string,
171
211
  keyValues: KeyValue[],
@@ -190,7 +230,7 @@ export async function putBulkKeyValue(
190
230
  }
191
231
  }
192
232
 
193
- export async function deleteBulkKeyValue(
233
+ export async function deleteKVBulkKeyValue(
194
234
  accountId: string,
195
235
  namespaceId: string,
196
236
  keys: string[],
@@ -215,7 +255,7 @@ export async function deleteBulkKeyValue(
215
255
  }
216
256
  }
217
257
 
218
- export function getNamespaceId(
258
+ export function getKVNamespaceId(
219
259
  { preview, binding, "namespace-id": namespaceId }: KvArgs,
220
260
  config: Config
221
261
  ): string {
@@ -310,7 +350,7 @@ export function getNamespaceId(
310
350
  /**
311
351
  * KV namespace binding names must be valid JS identifiers.
312
352
  */
313
- export function isValidNamespaceBinding(
353
+ export function isValidKVNamespaceBinding(
314
354
  binding: string | undefined
315
355
  ): binding is string {
316
356
  return (
package/src/pages.tsx CHANGED
@@ -4,7 +4,7 @@ import { execSync, spawn } from "node:child_process";
4
4
  import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { readdir, readFile, stat } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
- import { dirname, join, sep } from "node:path";
7
+ import { dirname, join, sep, extname, basename } from "node:path";
8
8
  import { cwd } from "node:process";
9
9
  import { URL } from "node:url";
10
10
  import { hash } from "blake3-wasm";
@@ -1079,8 +1079,7 @@ const createDeployment: CommandModule<
1079
1079
  const fileContent = await readFile(filepath);
1080
1080
 
1081
1081
  const base64Content = fileContent.toString("base64");
1082
- const extension =
1083
- name.split(".").length > 1 ? name.split(".").at(-1) || "" : "";
1082
+ const extension = extname(basename(name)).substring(1);
1084
1083
 
1085
1084
  const content = base64Content + extension;
1086
1085
 
@@ -1244,7 +1243,7 @@ const createDeployment: CommandModule<
1244
1243
  export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1245
1244
  return yargs
1246
1245
  .command(
1247
- "dev [directory] [-- command]",
1246
+ "dev [directory] [-- command..]",
1248
1247
  "🧑‍💻 Develop your full-stack Pages application locally",
1249
1248
  (yargs) => {
1250
1249
  return yargs
@@ -1576,6 +1575,10 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1576
1575
  default: false,
1577
1576
  description: "Build a plugin rather than a Worker script",
1578
1577
  },
1578
+ "build-output-directory": {
1579
+ type: "string",
1580
+ description: "The directory to output static assets to",
1581
+ },
1579
1582
  })
1580
1583
  .epilogue(pagesBetaWarning),
1581
1584
  async ({
@@ -1587,12 +1590,15 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1587
1590
  fallbackService,
1588
1591
  watch,
1589
1592
  plugin,
1593
+ "build-output-directory": buildOutputDirectory,
1590
1594
  }) => {
1591
1595
  if (!isInPagesCI) {
1592
1596
  // Beta message for `wrangler pages <commands>` usage
1593
1597
  logger.log(pagesBetaWarning);
1594
1598
  }
1595
1599
 
1600
+ buildOutputDirectory ??= dirname(outfile);
1601
+
1596
1602
  await buildFunctions({
1597
1603
  outfile,
1598
1604
  outputConfigPath,
@@ -1602,7 +1608,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1602
1608
  fallbackService,
1603
1609
  watch,
1604
1610
  plugin,
1605
- buildOutputDirectory: dirname(outfile),
1611
+ buildOutputDirectory,
1606
1612
  });
1607
1613
  }
1608
1614
  )