wrangler 2.0.3 → 2.0.7

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.
@@ -193,7 +193,8 @@ export function normalizeAndValidateConfig(
193
193
  dev: normalizeAndValidateDev(diagnostics, rawConfig.dev ?? {}),
194
194
  migrations: normalizeAndValidateMigrations(
195
195
  diagnostics,
196
- rawConfig.migrations ?? []
196
+ rawConfig.migrations ?? [],
197
+ activeEnv.durable_objects
197
198
  ),
198
199
  site: normalizeAndValidateSite(
199
200
  diagnostics,
@@ -387,7 +388,8 @@ function normalizeAndValidateDev(
387
388
  */
388
389
  function normalizeAndValidateMigrations(
389
390
  diagnostics: Diagnostics,
390
- rawMigrations: Config["migrations"]
391
+ rawMigrations: Config["migrations"],
392
+ durableObjects: Config["durable_objects"]
391
393
  ): Config["migrations"] {
392
394
  if (!Array.isArray(rawMigrations)) {
393
395
  diagnostics.errors.push(
@@ -441,6 +443,26 @@ function normalizeAndValidateMigrations(
441
443
  "string"
442
444
  );
443
445
  }
446
+
447
+ if (
448
+ Array.isArray(durableObjects?.bindings) &&
449
+ durableObjects.bindings.length > 0
450
+ ) {
451
+ // intrinsic [durable_objects] implies [migrations]
452
+ const exportedDurableObjects = (durableObjects.bindings || []).filter(
453
+ (binding) => !binding.script_name
454
+ );
455
+ if (exportedDurableObjects.length > 0 && rawMigrations.length === 0) {
456
+ diagnostics.warnings.push(
457
+ `In wrangler.toml, you have configured [durable_objects] exported by this Worker (${exportedDurableObjects
458
+ .map((durable) => durable.class_name || "(unnamed)")
459
+ .join(
460
+ ", "
461
+ )}), but no [migrations] for them. This may not work as expected until you add a [migrations] section to your wrangler.toml. Refer to https://developers.cloudflare.com/workers/learning/using-durable-objects/#durable-object-migrations-in-wranglertoml for more details.`
462
+ );
463
+ }
464
+ }
465
+
444
466
  return rawMigrations;
445
467
  }
446
468
  }
@@ -562,19 +584,37 @@ function normalizeAndValidateModulePaths(
562
584
  * or an object that looks like {pattern: string, zone_id: string }
563
585
  */
564
586
  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
- );
587
+ if (!item) {
588
+ return false;
589
+ }
590
+ if (typeof item === "string") {
591
+ return true;
592
+ }
593
+ if (typeof item === "object") {
594
+ if (!hasProperty(item, "pattern") || typeof item.pattern !== "string") {
595
+ return false;
596
+ }
597
+
598
+ const otherKeys = Object.keys(item).length - 1; // minus one to subtract "pattern"
599
+
600
+ const hasZoneId =
601
+ hasProperty(item, "zone_id") && typeof item.zone_id === "string";
602
+ const hasZoneName =
603
+ hasProperty(item, "zone_name") && typeof item.zone_name === "string";
604
+ const hasCustomDomainFlag =
605
+ hasProperty(item, "custom_domain") &&
606
+ typeof item.custom_domain === "boolean";
607
+
608
+ if (otherKeys === 2 && hasCustomDomainFlag && (hasZoneId || hasZoneName)) {
609
+ return true;
610
+ } else if (
611
+ otherKeys === 1 &&
612
+ (hasZoneId || hasZoneName || hasCustomDomainFlag)
613
+ ) {
614
+ return true;
615
+ }
616
+ }
617
+ return false;
578
618
  }
579
619
 
580
620
  /**
@@ -583,7 +623,7 @@ function isValidRouteValue(item: unknown): boolean {
583
623
  const isRoute: ValidatorFn = (diagnostics, field, value) => {
584
624
  if (value !== undefined && !isValidRouteValue(value)) {
585
625
  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(
626
+ `Expected "${field}" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name }, but got ${JSON.stringify(
587
627
  value
588
628
  )}.`
589
629
  );
@@ -613,7 +653,7 @@ const isRouteArray: ValidatorFn = (diagnostics, field, value) => {
613
653
  }
614
654
  if (invalidRoutes.length > 0) {
615
655
  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(
656
+ `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
657
  invalidRoutes,
618
658
  null,
619
659
  2
@@ -696,27 +736,12 @@ function normalizeAndValidateEnvironment(
696
736
  diagnostics,
697
737
  rawEnv,
698
738
  "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
- "```",
739
+ `The "experimental_services" field is no longer supported. Simply rename the [experimental_services] field to [services].`,
716
740
  true
717
741
  );
718
742
 
719
743
  experimental(diagnostics, rawEnv, "unsafe");
744
+ experimental(diagnostics, rawEnv, "services");
720
745
 
721
746
  const route = validateRoute(diagnostics, topLevelEnv, rawEnv);
722
747
 
@@ -880,6 +905,16 @@ function normalizeAndValidateEnvironment(
880
905
  validateBindingArray(envName, validateR2Binding),
881
906
  []
882
907
  ),
908
+ services: notInheritable(
909
+ diagnostics,
910
+ topLevelEnv,
911
+ rawConfig,
912
+ rawEnv,
913
+ envName,
914
+ "services",
915
+ validateBindingArray(envName, validateServiceBinding),
916
+ []
917
+ ),
883
918
  unsafe: notInheritable(
884
919
  diagnostics,
885
920
  topLevelEnv,
@@ -1014,7 +1049,7 @@ const validateRule: ValidatorFn = (diagnostics, field, value) => {
1014
1049
 
1015
1050
  if (!isOptionalProperty(rule, "fallthrough", "boolean")) {
1016
1051
  diagnostics.errors.push(
1017
- `binding should, optionally, have a boolean "fallthrough" field.`
1052
+ `the field "fallthrough", when present, should be a boolean.`
1018
1053
  );
1019
1054
  isValid = false;
1020
1055
  }
@@ -1135,7 +1170,7 @@ const validateDurableObjectBinding: ValidatorFn = (
1135
1170
  return false;
1136
1171
  }
1137
1172
 
1138
- // Durable Object bindings must have a name and class_name, and optionally a script_name.
1173
+ // Durable Object bindings must have a name and class_name, and optionally a script_name and an environment.
1139
1174
  let isValid = true;
1140
1175
  if (!isRequiredProperty(value, "name", "string")) {
1141
1176
  diagnostics.errors.push(`binding should have a string "name" field.`);
@@ -1147,7 +1182,21 @@ const validateDurableObjectBinding: ValidatorFn = (
1147
1182
  }
1148
1183
  if (!isOptionalProperty(value, "script_name", "string")) {
1149
1184
  diagnostics.errors.push(
1150
- `binding should, optionally, have a string "script_name" field.`
1185
+ `the field "script_name", when present, should be a string.`
1186
+ );
1187
+ isValid = false;
1188
+ }
1189
+ // environment requires a script_name
1190
+ if (!isOptionalProperty(value, "environment", "string")) {
1191
+ diagnostics.errors.push(
1192
+ `the field "environment", when present, should be a string.`
1193
+ );
1194
+ isValid = false;
1195
+ }
1196
+
1197
+ if ("environment" in value && !("script_name" in value)) {
1198
+ diagnostics.errors.push(
1199
+ `binding should have a "script_name" field if "environment" is present.`
1151
1200
  );
1152
1201
  isValid = false;
1153
1202
  }
@@ -1178,8 +1227,13 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
1178
1227
  const safeBindings = [
1179
1228
  "plain_text",
1180
1229
  "json",
1230
+ "wasm_module",
1231
+ "data_blob",
1232
+ "text_blob",
1181
1233
  "kv_namespace",
1182
1234
  "durable_object_namespace",
1235
+ "r2_bucket",
1236
+ "service",
1183
1237
  ];
1184
1238
 
1185
1239
  if (safeBindings.includes(value.type)) {
@@ -1422,3 +1476,38 @@ const validateBindingsHaveUniqueNames = (
1422
1476
 
1423
1477
  return !hasDuplicates;
1424
1478
  };
1479
+ const validateServiceBinding: ValidatorFn = (diagnostics, field, value) => {
1480
+ if (typeof value !== "object" || value === null) {
1481
+ diagnostics.errors.push(
1482
+ `"services" bindings should be objects, but got ${JSON.stringify(value)}`
1483
+ );
1484
+ return false;
1485
+ }
1486
+ let isValid = true;
1487
+ // Service bindings must have a binding, service, and environment.
1488
+ if (!isRequiredProperty(value, "binding", "string")) {
1489
+ diagnostics.errors.push(
1490
+ `"${field}" bindings should have a string "binding" field but got ${JSON.stringify(
1491
+ value
1492
+ )}.`
1493
+ );
1494
+ isValid = false;
1495
+ }
1496
+ if (!isRequiredProperty(value, "service", "string")) {
1497
+ diagnostics.errors.push(
1498
+ `"${field}" bindings should have a string "service" field but got ${JSON.stringify(
1499
+ value
1500
+ )}.`
1501
+ );
1502
+ isValid = false;
1503
+ }
1504
+ if (!isOptionalProperty(value, "environment", "string")) {
1505
+ diagnostics.errors.push(
1506
+ `"${field}" bindings should have a string "environment" field but got ${JSON.stringify(
1507
+ value
1508
+ )}.`
1509
+ );
1510
+ isValid = false;
1511
+ }
1512
+ return isValid;
1513
+ };
@@ -66,7 +66,12 @@ async function sessionToken(
66
66
  ? `/zones/${ctx.zone.id}/workers/edge-preview`
67
67
  : `/accounts/${accountId}/workers/subdomain/edge-preview`;
68
68
 
69
- const { exchange_url } = await fetchResult<{ exchange_url: string }>(initUrl);
69
+ const { exchange_url } = await fetchResult<{ exchange_url: string }>(
70
+ initUrl,
71
+ undefined,
72
+ undefined,
73
+ abortSignal
74
+ );
70
75
  const { inspector_websocket, prewarm, token } = (await (
71
76
  await fetch(exchange_url, { signal: abortSignal })
72
77
  ).json()) as { inspector_websocket: string; token: string; prewarm: string };
@@ -119,13 +124,18 @@ async function createPreviewToken(
119
124
  const formData = createWorkerUploadForm(worker);
120
125
  formData.set("wrangler-session-config", JSON.stringify(mode));
121
126
 
122
- const { preview_token } = await fetchResult<{ preview_token: string }>(url, {
123
- method: "POST",
124
- body: formData,
125
- headers: {
126
- "cf-preview-upload-config-token": value,
127
+ const { preview_token } = await fetchResult<{ preview_token: string }>(
128
+ url,
129
+ {
130
+ method: "POST",
131
+ body: formData,
132
+ headers: {
133
+ "cf-preview-upload-config-token": value,
134
+ },
127
135
  },
128
- });
136
+ undefined,
137
+ abortSignal
138
+ );
129
139
 
130
140
  return {
131
141
  value: preview_token,
@@ -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/dev.tsx CHANGED
@@ -9,7 +9,9 @@ import { withErrorBoundary, useErrorHandler } from "react-error-boundary";
9
9
  import onExit from "signal-exit";
10
10
  import tmp from "tmp-promise";
11
11
  import { fetch } from "undici";
12
+ import { printBindings } from "../config";
12
13
  import { runCustomBuild } from "../entry";
14
+ import { openInspector } from "../inspect";
13
15
  import { logger } from "../logger";
14
16
  import openInBrowser from "../open-in-browser";
15
17
  import { getAPIToken } from "../user";
@@ -104,6 +106,8 @@ export function DevImplementation(props: DevProps): JSX.Element {
104
106
  nodeCompat: props.nodeCompat,
105
107
  });
106
108
 
109
+ printBindings(props.bindings);
110
+
107
111
  // only load the UI if we're running in a supported environment
108
112
  const { isRawModeSupported } = useStdin();
109
113
  return isRawModeSupported ? (
@@ -378,10 +382,7 @@ function useHotkeys(
378
382
  }
379
383
  // toggle inspector
380
384
  case "d": {
381
- await openInBrowser(
382
- `https://built-devtools.pages.dev/js_app?experiments=true&v8only=true&ws=localhost:${inspectorPort}/ws`,
383
- { forceChromium: true }
384
- );
385
+ await openInspector(inspectorPort);
385
386
  break;
386
387
  }
387
388
  // toggle local
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,
@@ -185,7 +185,21 @@ export function useWorker(props: {
185
185
  // we want to log the error, but not end the process
186
186
  // since it could recover after the developer fixes whatever's wrong
187
187
  if ((err as { code: string }).code !== "ABORT_ERR") {
188
- logger.error("Error on remote worker:", err);
188
+ // instead of logging the raw API error to the user,
189
+ // give them friendly instructions
190
+ // for error 10063 (workers.dev subdomain required)
191
+ if (err.code === 10063) {
192
+ const errorMessage =
193
+ "Error: You need to register a workers.dev subdomain before running the dev command in remote mode";
194
+ const solutionMessage =
195
+ "You can either enable local mode by pressing l, or register a workers.dev subdomain here:";
196
+ const onboardingLink = `https://dash.cloudflare.com/${accountId}/workers/onboarding`;
197
+ logger.error(
198
+ `${errorMessage}\n${solutionMessage}\n${onboardingLink}`
199
+ );
200
+ } else {
201
+ logger.error("Error on remote worker:", err);
202
+ }
189
203
  }
190
204
  });
191
205
 
package/src/durable.ts ADDED
@@ -0,0 +1,102 @@
1
+ import assert from "node:assert";
2
+ import { fetchResult } from "./cfetch";
3
+ import { logger } from "./logger";
4
+ import type { Config } from "./config";
5
+ import type { CfWorkerInit } from "./worker";
6
+
7
+ /**
8
+ * For a given Worker + migrations config, figure out which migrations
9
+ * to upload based on the current migration tag of the deployed Worker.
10
+ */
11
+ export async function getMigrationsToUpload(
12
+ scriptName: string,
13
+ props: {
14
+ accountId: string | undefined;
15
+ config: Config;
16
+ legacyEnv: boolean | undefined;
17
+ env: string | undefined;
18
+ }
19
+ ): Promise<CfWorkerInit["migrations"]> {
20
+ const { config, accountId } = props;
21
+
22
+ assert(accountId, "Missing accountId");
23
+ // if config.migrations
24
+ let migrations;
25
+ if (config.migrations.length > 0) {
26
+ // get current migration tag
27
+ type ScriptData = { id: string; migration_tag?: string };
28
+ let script: ScriptData | undefined;
29
+ if (!props.legacyEnv) {
30
+ try {
31
+ if (props.env) {
32
+ const scriptData = await fetchResult<{
33
+ script: ScriptData;
34
+ }>(
35
+ `/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}`
36
+ );
37
+ script = scriptData.script;
38
+ } else {
39
+ const scriptData = await fetchResult<{
40
+ default_environment: {
41
+ script: ScriptData;
42
+ };
43
+ }>(`/accounts/${accountId}/workers/services/${scriptName}`);
44
+ script = scriptData.default_environment.script;
45
+ }
46
+ } catch (err) {
47
+ if (
48
+ ![
49
+ 10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all
50
+ 10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet
51
+ ].includes((err as { code: number }).code)
52
+ ) {
53
+ throw err;
54
+ }
55
+ // else it's a 404, no script found, and we can proceed
56
+ }
57
+ } else {
58
+ const scripts = await fetchResult<ScriptData[]>(
59
+ `/accounts/${accountId}/workers/scripts`
60
+ );
61
+ script = scripts.find(({ id }) => id === scriptName);
62
+ }
63
+
64
+ if (script?.migration_tag) {
65
+ // was already published once
66
+ const scriptMigrationTag = script.migration_tag;
67
+ const foundIndex = config.migrations.findIndex(
68
+ (migration) => migration.tag === scriptMigrationTag
69
+ );
70
+ if (foundIndex === -1) {
71
+ logger.warn(
72
+ `The published script ${scriptName} has a migration tag "${script.migration_tag}, which was not found in wrangler.toml. You may have already deleted it. Applying all available migrations to the script...`
73
+ );
74
+ migrations = {
75
+ old_tag: script.migration_tag,
76
+ new_tag: config.migrations[config.migrations.length - 1].tag,
77
+ steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
78
+ };
79
+ } else {
80
+ if (foundIndex !== config.migrations.length - 1) {
81
+ // there are new migrations to send up
82
+ migrations = {
83
+ old_tag: script.migration_tag,
84
+ new_tag: config.migrations[config.migrations.length - 1].tag,
85
+ steps: config.migrations
86
+ .slice(foundIndex + 1)
87
+ .map(({ tag: _tag, ...rest }) => rest),
88
+ };
89
+ }
90
+ // else, we're up to date, no migrations to send
91
+ }
92
+ } else {
93
+ // first time publishing durable objects to this script,
94
+ // so we send all the migrations
95
+ migrations = {
96
+ new_tag: config.migrations[config.migrations.length - 1].tag,
97
+ steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
98
+ };
99
+ }
100
+ }
101
+ return migrations;
102
+ }