wrangler 2.0.5 → 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.
@@ -8,6 +8,24 @@ export interface Environment
8
8
  extends EnvironmentInheritable,
9
9
  EnvironmentNonInheritable {}
10
10
 
11
+ export type SimpleRoute = string;
12
+ export type ZoneIdRoute = {
13
+ pattern: string;
14
+ zone_id: string;
15
+ custom_domain?: boolean;
16
+ };
17
+ export type ZoneNameRoute = {
18
+ pattern: string;
19
+ zone_name: string;
20
+ custom_domain?: boolean;
21
+ };
22
+ export type CustomDomainRoute = { pattern: string; custom_domain: boolean };
23
+ export type Route =
24
+ | SimpleRoute
25
+ | ZoneIdRoute
26
+ | ZoneNameRoute
27
+ | CustomDomainRoute;
28
+
11
29
  /**
12
30
  * The `EnvironmentInheritable` interface declares all the configuration fields for an environment
13
31
  * that can be inherited (and overridden) from the top-level environment.
@@ -74,13 +92,7 @@ interface EnvironmentInheritable {
74
92
  *
75
93
  * @inheritable
76
94
  */
77
- routes:
78
- | (
79
- | string
80
- | { pattern: string; zone_id: string }
81
- | { pattern: string; zone_name: string }
82
- )[]
83
- | undefined;
95
+ routes: Route[] | undefined;
84
96
 
85
97
  /**
86
98
  * A route that your worker should be published to. Literally
@@ -91,13 +103,7 @@ interface EnvironmentInheritable {
91
103
  *
92
104
  * @inheritable
93
105
  */
94
- route:
95
- | (
96
- | string
97
- | { pattern: string; zone_id: string }
98
- | { pattern: string; zone_name: string }
99
- )
100
- | undefined;
106
+ route: Route | undefined;
101
107
 
102
108
  /**
103
109
  * Path to a custom tsconfig
@@ -236,6 +242,8 @@ interface EnvironmentNonInheritable {
236
242
  class_name: string;
237
243
  /** The script where the Durable Object is defined (if it's external to this worker) */
238
244
  script_name?: string;
245
+ /** The service environment of the script_name to bind to */
246
+ environment?: string;
239
247
  }[];
240
248
  };
241
249
 
@@ -279,6 +287,24 @@ interface EnvironmentNonInheritable {
279
287
  preview_bucket_name?: string;
280
288
  }[];
281
289
 
290
+ /**
291
+ * Specifies service bindings (worker-to-worker) that are bound to this Worker environment.
292
+ *
293
+ * NOTE: This field is not automatically inherited from the top level environment,
294
+ * and so must be specified in every named environment.
295
+ *
296
+ * @default `[]`
297
+ * @nonInheritable
298
+ */
299
+ services: {
300
+ /** The binding name used to refer to the bound service. */
301
+ binding: string;
302
+ /** The name of the service. */
303
+ service: string;
304
+ /** The environment of the service (e.g. production, staging, etc). */
305
+ environment?: string;
306
+ }[];
307
+
282
308
  /**
283
309
  * "Unsafe" tables for features that aren't directly supported by wrangler.
284
310
  *
@@ -562,19 +562,37 @@ function normalizeAndValidateModulePaths(
562
562
  * or an object that looks like {pattern: string, zone_id: string }
563
563
  */
564
564
  function isValidRouteValue(item: unknown): boolean {
565
- return (
566
- !!item &&
567
- (typeof item === "string" ||
568
- (typeof item === "object" &&
569
- hasProperty(item, "pattern") &&
570
- typeof item.pattern === "string" &&
571
- // it could have a zone_name
572
- ((hasProperty(item, "zone_name") &&
573
- typeof item.zone_name === "string") ||
574
- // or a zone_id
575
- (hasProperty(item, "zone_id") && typeof item.zone_id === "string")) &&
576
- Object.keys(item).length === 2))
577
- );
565
+ if (!item) {
566
+ return false;
567
+ }
568
+ if (typeof item === "string") {
569
+ return true;
570
+ }
571
+ if (typeof item === "object") {
572
+ if (!hasProperty(item, "pattern") || typeof item.pattern !== "string") {
573
+ return false;
574
+ }
575
+
576
+ const otherKeys = Object.keys(item).length - 1; // minus one to subtract "pattern"
577
+
578
+ const hasZoneId =
579
+ hasProperty(item, "zone_id") && typeof item.zone_id === "string";
580
+ const hasZoneName =
581
+ hasProperty(item, "zone_name") && typeof item.zone_name === "string";
582
+ const hasCustomDomainFlag =
583
+ hasProperty(item, "custom_domain") &&
584
+ typeof item.custom_domain === "boolean";
585
+
586
+ if (otherKeys === 2 && hasCustomDomainFlag && (hasZoneId || hasZoneName)) {
587
+ return true;
588
+ } else if (
589
+ otherKeys === 1 &&
590
+ (hasZoneId || hasZoneName || hasCustomDomainFlag)
591
+ ) {
592
+ return true;
593
+ }
594
+ }
595
+ return false;
578
596
  }
579
597
 
580
598
  /**
@@ -583,7 +601,7 @@ function isValidRouteValue(item: unknown): boolean {
583
601
  const isRoute: ValidatorFn = (diagnostics, field, value) => {
584
602
  if (value !== undefined && !isValidRouteValue(value)) {
585
603
  diagnostics.errors.push(
586
- `Expected "${field}" to be either a string, or an object with shape { pattern, zone_id | zone_name }, but got ${JSON.stringify(
604
+ `Expected "${field}" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name }, but got ${JSON.stringify(
587
605
  value
588
606
  )}.`
589
607
  );
@@ -613,7 +631,7 @@ const isRouteArray: ValidatorFn = (diagnostics, field, value) => {
613
631
  }
614
632
  if (invalidRoutes.length > 0) {
615
633
  diagnostics.errors.push(
616
- `Expected "${field}" to be an array of either strings or objects with the shape { pattern, zone_id | zone_name }, but these weren't valid: ${JSON.stringify(
634
+ `Expected "${field}" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name }, but these weren't valid: ${JSON.stringify(
617
635
  invalidRoutes,
618
636
  null,
619
637
  2
@@ -696,27 +714,12 @@ function normalizeAndValidateEnvironment(
696
714
  diagnostics,
697
715
  rawEnv,
698
716
  "experimental_services",
699
- `The "experimental_services" field is no longer supported. Instead, use [[unsafe.bindings]] to enable experimental features. Add this to your wrangler.toml:\n` +
700
- "```\n" +
701
- TOML.stringify({
702
- unsafe: {
703
- bindings: (rawEnv?.experimental_services || []).map(
704
- (serviceDefinition) => {
705
- return {
706
- name: serviceDefinition.name,
707
- type: "service",
708
- service: serviceDefinition.service,
709
- environment: serviceDefinition.environment,
710
- };
711
- }
712
- ),
713
- },
714
- }) +
715
- "```",
717
+ `The "experimental_services" field is no longer supported. Simply rename the [experimental_services] field to [services].`,
716
718
  true
717
719
  );
718
720
 
719
721
  experimental(diagnostics, rawEnv, "unsafe");
722
+ experimental(diagnostics, rawEnv, "services");
720
723
 
721
724
  const route = validateRoute(diagnostics, topLevelEnv, rawEnv);
722
725
 
@@ -880,6 +883,16 @@ function normalizeAndValidateEnvironment(
880
883
  validateBindingArray(envName, validateR2Binding),
881
884
  []
882
885
  ),
886
+ services: notInheritable(
887
+ diagnostics,
888
+ topLevelEnv,
889
+ rawConfig,
890
+ rawEnv,
891
+ envName,
892
+ "services",
893
+ validateBindingArray(envName, validateServiceBinding),
894
+ []
895
+ ),
883
896
  unsafe: notInheritable(
884
897
  diagnostics,
885
898
  topLevelEnv,
@@ -1014,7 +1027,7 @@ const validateRule: ValidatorFn = (diagnostics, field, value) => {
1014
1027
 
1015
1028
  if (!isOptionalProperty(rule, "fallthrough", "boolean")) {
1016
1029
  diagnostics.errors.push(
1017
- `binding should, optionally, have a boolean "fallthrough" field.`
1030
+ `the field "fallthrough", when present, should be a boolean.`
1018
1031
  );
1019
1032
  isValid = false;
1020
1033
  }
@@ -1135,7 +1148,7 @@ const validateDurableObjectBinding: ValidatorFn = (
1135
1148
  return false;
1136
1149
  }
1137
1150
 
1138
- // Durable Object bindings must have a name and class_name, and optionally a script_name.
1151
+ // Durable Object bindings must have a name and class_name, and optionally a script_name and an environment.
1139
1152
  let isValid = true;
1140
1153
  if (!isRequiredProperty(value, "name", "string")) {
1141
1154
  diagnostics.errors.push(`binding should have a string "name" field.`);
@@ -1147,7 +1160,21 @@ const validateDurableObjectBinding: ValidatorFn = (
1147
1160
  }
1148
1161
  if (!isOptionalProperty(value, "script_name", "string")) {
1149
1162
  diagnostics.errors.push(
1150
- `binding should, optionally, have a string "script_name" field.`
1163
+ `the field "script_name", when present, should be a string.`
1164
+ );
1165
+ isValid = false;
1166
+ }
1167
+ // environment requires a script_name
1168
+ if (!isOptionalProperty(value, "environment", "string")) {
1169
+ diagnostics.errors.push(
1170
+ `the field "environment", when present, should be a string.`
1171
+ );
1172
+ isValid = false;
1173
+ }
1174
+
1175
+ if ("environment" in value && !("script_name" in value)) {
1176
+ diagnostics.errors.push(
1177
+ `binding should have a "script_name" field if "environment" is present.`
1151
1178
  );
1152
1179
  isValid = false;
1153
1180
  }
@@ -1178,8 +1205,13 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
1178
1205
  const safeBindings = [
1179
1206
  "plain_text",
1180
1207
  "json",
1208
+ "wasm_module",
1209
+ "data_blob",
1210
+ "text_blob",
1181
1211
  "kv_namespace",
1182
1212
  "durable_object_namespace",
1213
+ "r2_bucket",
1214
+ "service",
1183
1215
  ];
1184
1216
 
1185
1217
  if (safeBindings.includes(value.type)) {
@@ -1422,3 +1454,38 @@ const validateBindingsHaveUniqueNames = (
1422
1454
 
1423
1455
  return !hasDuplicates;
1424
1456
  };
1457
+ const validateServiceBinding: ValidatorFn = (diagnostics, field, value) => {
1458
+ if (typeof value !== "object" || value === null) {
1459
+ diagnostics.errors.push(
1460
+ `"services" bindings should be objects, but got ${JSON.stringify(value)}`
1461
+ );
1462
+ return false;
1463
+ }
1464
+ let isValid = true;
1465
+ // Service bindings must have a binding, service, and environment.
1466
+ if (!isRequiredProperty(value, "binding", "string")) {
1467
+ diagnostics.errors.push(
1468
+ `"${field}" bindings should have a string "binding" field but got ${JSON.stringify(
1469
+ value
1470
+ )}.`
1471
+ );
1472
+ isValid = false;
1473
+ }
1474
+ if (!isRequiredProperty(value, "service", "string")) {
1475
+ diagnostics.errors.push(
1476
+ `"${field}" bindings should have a string "service" field but got ${JSON.stringify(
1477
+ value
1478
+ )}.`
1479
+ );
1480
+ isValid = false;
1481
+ }
1482
+ if (!isOptionalProperty(value, "environment", "string")) {
1483
+ diagnostics.errors.push(
1484
+ `"${field}" bindings should have a string "environment" field but got ${JSON.stringify(
1485
+ value
1486
+ )}.`
1487
+ );
1488
+ isValid = false;
1489
+ }
1490
+ return isValid;
1491
+ };
@@ -32,20 +32,24 @@ export interface WorkerMetadata {
32
32
  compatibility_flags?: string[];
33
33
  usage_model?: "bundled" | "unbound";
34
34
  migrations?: CfDurableObjectMigrations;
35
+ // If you add any new binding types here, also add it to safeBindings
36
+ // under validateUnsafeBinding in config/validation.ts
35
37
  bindings: (
36
- | { type: "kv_namespace"; name: string; namespace_id: string }
37
38
  | { type: "plain_text"; name: string; text: string }
38
39
  | { type: "json"; name: string; json: unknown }
39
40
  | { type: "wasm_module"; name: string; part: string }
40
41
  | { type: "text_blob"; name: string; part: string }
41
42
  | { type: "data_blob"; name: string; part: string }
43
+ | { type: "kv_namespace"; name: string; namespace_id: string }
42
44
  | {
43
45
  type: "durable_object_namespace";
44
46
  name: string;
45
47
  class_name: string;
46
48
  script_name?: string;
49
+ environment?: string;
47
50
  }
48
51
  | { type: "r2_bucket"; name: string; bucket_name: string }
52
+ | { type: "service"; name: string; service: string; environment?: string }
49
53
  )[];
50
54
  }
51
55
 
@@ -67,6 +71,14 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
67
71
 
68
72
  const metadataBindings: WorkerMetadata["bindings"] = [];
69
73
 
74
+ Object.entries(bindings.vars || {})?.forEach(([key, value]) => {
75
+ if (typeof value === "string") {
76
+ metadataBindings.push({ name: key, type: "plain_text", text: value });
77
+ } else {
78
+ metadataBindings.push({ name: key, type: "json", json: value });
79
+ }
80
+ });
81
+
70
82
  bindings.kv_namespaces?.forEach(({ id, binding }) => {
71
83
  metadataBindings.push({
72
84
  name: binding,
@@ -76,12 +88,13 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
76
88
  });
77
89
 
78
90
  bindings.durable_objects?.bindings.forEach(
79
- ({ name, class_name, script_name }) => {
91
+ ({ name, class_name, script_name, environment }) => {
80
92
  metadataBindings.push({
81
93
  name,
82
94
  type: "durable_object_namespace",
83
95
  class_name: class_name,
84
96
  ...(script_name && { script_name }),
97
+ ...(environment && { environment }),
85
98
  });
86
99
  }
87
100
  );
@@ -94,12 +107,13 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
94
107
  });
95
108
  });
96
109
 
97
- Object.entries(bindings.vars || {})?.forEach(([key, value]) => {
98
- if (typeof value === "string") {
99
- metadataBindings.push({ name: key, type: "plain_text", text: value });
100
- } else {
101
- metadataBindings.push({ name: key, type: "json", json: value });
102
- }
110
+ bindings.services?.forEach(({ binding, service, environment }) => {
111
+ metadataBindings.push({
112
+ name: binding,
113
+ type: "service",
114
+ service,
115
+ ...(environment && { environment }),
116
+ });
103
117
  });
104
118
 
105
119
  for (const [name, filePath] of Object.entries(bindings.wasm_modules || {})) {
package/src/dev/local.tsx CHANGED
@@ -87,6 +87,11 @@ function useLocalWorker({
87
87
  '⎔ A "public" folder is not yet supported in local mode.'
88
88
  );
89
89
  }
90
+ if (bindings.services && bindings.services.length > 0) {
91
+ throw new Error(
92
+ "⎔ Service bindings are not yet supported in local mode."
93
+ );
94
+ }
90
95
 
91
96
  // In local mode, we want to copy all referenced modules into
92
97
  // the output bundle directory before starting up
@@ -297,6 +302,7 @@ function useLocalWorker({
297
302
  bindings.durable_objects?.bindings,
298
303
  bindings.kv_namespaces,
299
304
  bindings.vars,
305
+ bindings.services,
300
306
  compatibilityDate,
301
307
  compatibilityFlags,
302
308
  localPersistencePath,
package/src/index.tsx CHANGED
@@ -206,9 +206,17 @@ function demandOneOfOption(...options: string[]) {
206
206
  };
207
207
  }
208
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
+
209
217
  class CommandLineArgsError extends Error {}
210
218
 
211
- export async function main(argv: string[]): Promise<void> {
219
+ function createCLIParser(argv: string[]) {
212
220
  const wrangler = makeCLI(argv)
213
221
  .strict()
214
222
  // We handle errors ourselves in a try-catch around `yargs.parse`.
@@ -413,12 +421,7 @@ export async function main(argv: string[]): Promise<void> {
413
421
  try {
414
422
  isGitInstalled = (await execa("git", ["--version"])).exitCode === 0;
415
423
  } catch (err) {
416
- if ((err as { code: string | undefined }).code !== "ENOENT") {
417
- // only throw if the error is not because git is not installed
418
- throw err;
419
- } else {
420
- isGitInstalled = false;
421
- }
424
+ isGitInstalled = false;
422
425
  }
423
426
  if (!isInsideGitProject && isGitInstalled) {
424
427
  const shouldInitGit =
@@ -897,6 +900,19 @@ export async function main(argv: string[]): Promise<void> {
897
900
  const config = readConfig(configPath, args);
898
901
  const entry = await getEntry(args, config, "dev");
899
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
+
900
916
  if (args.inspect) {
901
917
  logger.warn(
902
918
  "Passing --inspect is unnecessary, now you can always connect to devtools."
@@ -934,6 +950,9 @@ export async function main(argv: string[]): Promise<void> {
934
950
  * try to extract a host from it
935
951
  */
936
952
  function getHost(urlLike: string): string | undefined {
953
+ // strip leading * / *.
954
+ urlLike = urlLike.replace(/^\*(\.)?/g, "");
955
+
937
956
  if (
938
957
  !(urlLike.startsWith("http://") || urlLike.startsWith("https://"))
939
958
  ) {
@@ -1104,6 +1123,7 @@ export async function main(argv: string[]): Promise<void> {
1104
1123
  };
1105
1124
  }
1106
1125
  ),
1126
+ services: config.services,
1107
1127
  unsafe: config.unsafe?.bindings,
1108
1128
  }}
1109
1129
  crons={config.triggers.crons}
@@ -1258,7 +1278,7 @@ export async function main(argv: string[]): Promise<void> {
1258
1278
  );
1259
1279
  }
1260
1280
 
1261
- const accountId = await requireAuth(config);
1281
+ const accountId = args.dryRun ? undefined : await requireAuth(config);
1262
1282
 
1263
1283
  const assetPaths = getAssetPaths(
1264
1284
  config,
@@ -1556,6 +1576,7 @@ export async function main(argv: string[]): Promise<void> {
1556
1576
  };
1557
1577
  }
1558
1578
  ),
1579
+ services: config.services,
1559
1580
  unsafe: config.unsafe?.bindings,
1560
1581
  }}
1561
1582
  crons={config.triggers.crons}
@@ -1706,9 +1727,11 @@ export async function main(argv: string[]): Promise<void> {
1706
1727
  const accountId = await requireAuth(config);
1707
1728
 
1708
1729
  const isInteractive = process.stdin.isTTY;
1709
- const secretValue = isInteractive
1710
- ? await prompt("Enter a secret value:", "password")
1711
- : await readFromStdin();
1730
+ const secretValue = trimTrailingWhitespace(
1731
+ isInteractive
1732
+ ? await prompt("Enter a secret value:", "password")
1733
+ : await readFromStdin()
1734
+ );
1712
1735
 
1713
1736
  logger.log(
1714
1737
  `🌀 Creating the secret for script ${scriptName} ${
@@ -1753,6 +1776,7 @@ export async function main(argv: string[]): Promise<void> {
1753
1776
  vars: {},
1754
1777
  durable_objects: { bindings: [] },
1755
1778
  r2_buckets: [],
1779
+ services: [],
1756
1780
  wasm_modules: {},
1757
1781
  text_blobs: {},
1758
1782
  data_blobs: {},
@@ -2334,13 +2358,7 @@ export async function main(argv: string[]): Promise<void> {
2334
2358
  const warnings: string[] = [];
2335
2359
  for (let i = 0; i < content.length; i++) {
2336
2360
  const keyValue = content[i];
2337
- if (typeof keyValue !== "object") {
2338
- errors.push(
2339
- `The item at index ${i} is type: "${typeof keyValue}" - ${JSON.stringify(
2340
- keyValue
2341
- )}`
2342
- );
2343
- } else if (!isKVKeyValue(keyValue)) {
2361
+ if (!isKVKeyValue(keyValue)) {
2344
2362
  errors.push(
2345
2363
  `The item at index ${i} is ${JSON.stringify(keyValue)}`
2346
2364
  );
@@ -2647,14 +2665,21 @@ export async function main(argv: string[]): Promise<void> {
2647
2665
  wrangler.version(wranglerVersion).alias("v", "version");
2648
2666
  wrangler.exitProcess(false);
2649
2667
 
2668
+ return wrangler;
2669
+ }
2670
+
2671
+ export async function main(argv: string[]): Promise<void> {
2672
+ const wrangler = createCLIParser(argv);
2650
2673
  try {
2651
2674
  await wrangler.parse();
2652
2675
  } catch (e) {
2653
2676
  logger.log(""); // Just adds a bit of space
2654
2677
  if (e instanceof CommandLineArgsError) {
2655
- wrangler.showHelp("error");
2656
- logger.log(""); // Add a bit of space.
2657
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();
2658
2683
  } else if (e instanceof ParseError) {
2659
2684
  e.notes.push({
2660
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
@@ -129,9 +129,24 @@ const KeyValueKeys = new Set([
129
129
  /**
130
130
  * Is the given object a valid `KeyValue` type?
131
131
  */
132
- export function isKVKeyValue(keyValue: object): keyValue is KeyValue {
133
- const props = Object.keys(keyValue);
134
- 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
+ ) {
135
150
  return false;
136
151
  }
137
152
  return true;
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