wrangler 0.0.0-e3cab74 → 0.0.0-e3e3243

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 (66) hide show
  1. package/README.md +1 -1
  2. package/bin/wrangler.js +10 -1
  3. package/miniflare-dist/index.mjs +79 -0
  4. package/miniflare-dist/index.mjs.map +7 -0
  5. package/package.json +10 -7
  6. package/pages/functions/filepath-routing.test.ts +125 -25
  7. package/pages/functions/filepath-routing.ts +6 -3
  8. package/src/__tests__/configuration.test.ts +959 -324
  9. package/src/__tests__/dev.test.tsx +512 -84
  10. package/src/__tests__/guess-worker-format.test.ts +19 -17
  11. package/src/__tests__/helpers/mock-account-id.ts +22 -4
  12. package/src/__tests__/helpers/mock-cfetch.ts +3 -3
  13. package/src/__tests__/helpers/mock-console.ts +17 -9
  14. package/src/__tests__/helpers/mock-kv.ts +9 -16
  15. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  16. package/src/__tests__/helpers/run-in-tmp.ts +3 -0
  17. package/src/__tests__/https-options.test.ts +127 -0
  18. package/src/__tests__/index.test.ts +128 -11
  19. package/src/__tests__/jest.setup.ts +16 -4
  20. package/src/__tests__/kv.test.ts +364 -125
  21. package/src/__tests__/logout.test.ts +15 -13
  22. package/src/__tests__/pages.test.ts +82 -0
  23. package/src/__tests__/publish.test.ts +1838 -735
  24. package/src/__tests__/r2.test.ts +4 -4
  25. package/src/__tests__/route.test.ts +45 -0
  26. package/src/__tests__/secret.test.ts +167 -68
  27. package/src/__tests__/tail.test.ts +2 -4
  28. package/src/__tests__/whoami.test.tsx +21 -7
  29. package/src/bundle.ts +27 -5
  30. package/src/cfetch/internal.ts +11 -5
  31. package/src/config/README.md +6 -6
  32. package/src/config/config.ts +54 -101
  33. package/src/config/environment.ts +67 -1
  34. package/src/config/index.ts +13 -3
  35. package/src/config/validation-helpers.ts +207 -23
  36. package/src/config/validation.ts +344 -129
  37. package/src/{api/preview.ts → create-worker-preview.ts} +39 -20
  38. package/src/{api/form_data.ts → create-worker-upload-form.ts} +1 -1
  39. package/src/dev/dev.tsx +392 -0
  40. package/src/dev/local.tsx +232 -0
  41. package/src/dev/remote.tsx +203 -0
  42. package/src/dev/use-esbuild.ts +104 -0
  43. package/src/entry.ts +192 -9
  44. package/src/https-options.ts +122 -0
  45. package/src/index.tsx +577 -515
  46. package/src/inspect.ts +9 -1
  47. package/src/intl-polyfill.d.ts +139 -0
  48. package/src/{kv.tsx → kv.ts} +97 -53
  49. package/src/miniflare-cli/README.md +30 -0
  50. package/src/miniflare-cli/enum-keys.ts +17 -0
  51. package/src/miniflare-cli/index.ts +37 -0
  52. package/src/module-collection.ts +90 -2
  53. package/src/open-in-browser.ts +14 -2
  54. package/src/pages.tsx +26 -10
  55. package/src/proxy.ts +104 -40
  56. package/src/publish.ts +53 -71
  57. package/src/reporting.ts +25 -9
  58. package/src/selfsigned.d.ts +29 -0
  59. package/src/sites.tsx +68 -29
  60. package/src/user.tsx +163 -85
  61. package/src/{api/worker.ts → worker.ts} +1 -24
  62. package/wrangler-dist/cli.js +5933 -3842
  63. package/wrangler-dist/cli.js.map +3 -3
  64. package/src/__tests__/helpers/mock-user.ts +0 -27
  65. package/src/dev.tsx +0 -961
  66. package/src/guess-worker-format.ts +0 -68
@@ -3,7 +3,7 @@ import type { Environment, RawEnvironment } from "./environment";
3
3
  /**
4
4
  * This is the static type definition for the configuration object.
5
5
  *
6
- * It reflects the configuration that you can write in wrangler.toml,
6
+ * It reflects a normalized and validated version of the configuration that you can write in wrangler.toml,
7
7
  * and optionally augment with arguments passed directly to wrangler.
8
8
  *
9
9
  * For more information about the configuration object, see the
@@ -20,20 +20,16 @@ import type { Environment, RawEnvironment } from "./environment";
20
20
  * - `@breaking`: the deprecation/optionality is a breaking change from wrangler 1.
21
21
  * - `@todo`: there's more work to be done (with details attached).
22
22
  */
23
- export type Config = ConfigFields<Environment> & Environment;
23
+ export type Config = ConfigFields<DevConfig> & Environment;
24
24
 
25
- export type RawConfig = Partial<ConfigFields<RawEnvironment>> &
25
+ export type RawConfig = Partial<ConfigFields<RawDevConfig>> &
26
26
  RawEnvironment &
27
- DeprecatedConfigFields;
27
+ DeprecatedConfigFields &
28
+ EnvironmentMap;
28
29
 
29
- export interface ConfigFields<Env extends RawEnvironment> {
30
+ export interface ConfigFields<Dev extends RawDevConfig> {
30
31
  configPath: string | undefined;
31
32
 
32
- /**
33
- * The entrypoint/path to the JavaScript file that will be executed.
34
- */
35
- main: string | undefined;
36
-
37
33
  /**
38
34
  * A boolean to enable "legacy" style wrangler environments (from wrangler 1).
39
35
  * These have been superseded by Services, but there may be projects that won't
@@ -42,59 +38,10 @@ export interface ConfigFields<Env extends RawEnvironment> {
42
38
  */
43
39
  legacy_env: boolean;
44
40
 
45
- /**
46
- * The `env` section defines overrides for the configuration for different environments.
47
- *
48
- * All environment fields can be specified at the top level of the config indicating the default environment settings.
49
- *
50
- * - Some fields are inherited and overridable in each environment.
51
- * - But some are not inherited and must be explicitly specified in every environment, if they are specified at the top level.
52
- *
53
- * For more information, see the documentation at https://developers.cloudflare.com/workers/cli-wrangler/configuration#environments
54
- *
55
- * @default `{}`
56
- */
57
- env: {
58
- [envName: string]: Env;
59
- };
60
-
61
41
  /**
62
42
  * Options to configure the development server that your worker will use.
63
- *
64
- * @default `{}`
65
43
  */
66
- dev: {
67
- /**
68
- * IP address for the local dev server to listen on,
69
- *
70
- * @default `127.0.0.1`
71
- * @todo this needs to be implemented
72
- */
73
- ip?: string;
74
-
75
- /**
76
- * Port for the local dev server to listen on
77
- *
78
- * @default `8787`
79
- */
80
- port?: number;
81
-
82
- /**
83
- * Protocol that local wrangler dev server listens to requests on.
84
- *
85
- * @default `http`
86
- * @todo this needs to be implemented
87
- */
88
- local_protocol?: string;
89
-
90
- /**
91
- * Protocol that wrangler dev forwards requests on
92
- *
93
- * @default `https`
94
- * @todo this needs to be implemented
95
- */
96
- upstream_protocol?: string;
97
- };
44
+ dev: Dev;
98
45
 
99
46
  /**
100
47
  * A list of migrations that should be uploaded with your Worker.
@@ -188,27 +135,50 @@ export interface ConfigFields<Env extends RawEnvironment> {
188
135
  [key: string]: string;
189
136
  }
190
137
  | undefined;
138
+ }
191
139
 
140
+ export interface DevConfig {
192
141
  /**
193
- * Configures a custom build step to be run by Wrangler when building your Worker.
142
+ * IP address for the local dev server to listen on,
194
143
  *
195
- * Refer to the [custom builds documentation](https://developers.cloudflare.com/workers/cli-wrangler/configuration#build)
196
- * for more details.
144
+ * @default `localhost`
145
+ * @todo this needs to be implemented
146
+ */
147
+ ip: string;
148
+
149
+ /**
150
+ * Port for the local dev server to listen on
197
151
  *
198
- * @default {}
152
+ * @default `8787`
199
153
  */
200
- build: {
201
- /** The command used to build your Worker. On Linux and macOS, the command is executed in the `sh` shell and the `cmd` shell for Windows. The `&&` and `||` shell operators may be used. */
202
- command?: string;
203
- /** The directory in which the command is executed. */
204
- cwd?: string;
205
- /** The directory to watch for changes while using wrangler dev, defaults to the current working directory */
206
- watch_dir?: string;
207
- /** Deprecated field previously used to configure the build and upload of the script. */
208
- upload?: DeprecatedUpload;
209
- };
154
+ port: number;
155
+
156
+ /**
157
+ * Protocol that local wrangler dev server listens to requests on.
158
+ *
159
+ * @default `http`
160
+ * @todo this needs to be implemented
161
+ */
162
+ local_protocol: "http" | "https";
163
+
164
+ /**
165
+ * Protocol that wrangler dev forwards requests on
166
+ *
167
+ * Setting this to `http` is not currently implemented.
168
+ * See https://github.com/cloudflare/wrangler2/issues/583
169
+ *
170
+ * @default `https`
171
+ */
172
+ upstream_protocol: "https" | "http";
173
+
174
+ /**
175
+ * Host to forward requests to, defaults to the zone of project
176
+ */
177
+ host: string | undefined;
210
178
  }
211
179
 
180
+ export type RawDevConfig = Partial<DevConfig>;
181
+
212
182
  export interface DeprecatedConfigFields {
213
183
  /**
214
184
  * The project "type". A holdover from wrangler 1.x.
@@ -229,37 +199,20 @@ export interface DeprecatedConfigFields {
229
199
  webpack_config?: string;
230
200
  }
231
201
 
232
- /**
233
- * Deprecated upload configuration.
234
- */
235
- export interface DeprecatedUpload {
202
+ interface EnvironmentMap {
236
203
  /**
237
- * The format of the Worker script.
204
+ * The `env` section defines overrides for the configuration for different environments.
238
205
  *
239
- * @deprecated We infer the format automatically now.
240
- */
241
- format?: "modules" | "service-worker";
242
-
243
- /**
244
- * The directory you wish to upload your worker from,
245
- * relative to the wrangler.toml file.
206
+ * All environment fields can be specified at the top level of the config indicating the default environment settings.
246
207
  *
247
- * Defaults to the directory containing the wrangler.toml file.
208
+ * - Some fields are inherited and overridable in each environment.
209
+ * - But some are not inherited and must be explicitly specified in every environment, if they are specified at the top level.
248
210
  *
249
- * @deprecated
250
- * @breaking In wrangler 1, this defaults to ./dist, whereas in wrangler 2 it defaults to ./
251
- */
252
- dir?: string;
253
-
254
- /**
255
- * The path to the Worker script, relative to `upload.dir`.
211
+ * For more information, see the documentation at https://developers.cloudflare.com/workers/cli-wrangler/configuration#environments
256
212
  *
257
- * @deprecated This will be replaced by a command line argument.
258
- */
259
- main?: string;
260
-
261
- /**
262
- * @deprecated This is now defined at the top level `rules` field.
213
+ * @default `{}`
263
214
  */
264
- rules?: Environment["rules"];
215
+ env?: {
216
+ [envName: string]: RawEnvironment;
217
+ };
265
218
  }
@@ -51,6 +51,11 @@ interface EnvironmentInheritable {
51
51
  */
52
52
  compatibility_flags: string[];
53
53
 
54
+ /**
55
+ * The entrypoint/path to the JavaScript file that will be executed.
56
+ */
57
+ main: string | undefined;
58
+
54
59
  /**
55
60
  * Whether we use <name>.<subdomain>.workers.dev to
56
61
  * test and deploy your worker.
@@ -59,7 +64,7 @@ interface EnvironmentInheritable {
59
64
  * @breaking
60
65
  * @inheritable
61
66
  */
62
- workers_dev: boolean;
67
+ workers_dev: boolean | undefined;
63
68
 
64
69
  /**
65
70
  * A list of routes that your worker should be published to.
@@ -82,6 +87,11 @@ interface EnvironmentInheritable {
82
87
  */
83
88
  route: string | undefined;
84
89
 
90
+ /**
91
+ * Path to a custom tsconfig
92
+ */
93
+ tsconfig: string | undefined;
94
+
85
95
  /**
86
96
  * The function to use to replace jsx syntax.
87
97
  *
@@ -133,6 +143,28 @@ interface EnvironmentInheritable {
133
143
  */
134
144
  rules: Rule[];
135
145
 
146
+ /**
147
+ * Configures a custom build step to be run by Wrangler when building your Worker.
148
+ *
149
+ * Refer to the [custom builds documentation](https://developers.cloudflare.com/workers/cli-wrangler/configuration#build)
150
+ * for more details.
151
+ *
152
+ * @default {}
153
+ */
154
+ build: {
155
+ /** The command used to build your Worker. On Linux and macOS, the command is executed in the `sh` shell and the `cmd` shell for Windows. The `&&` and `||` shell operators may be used. */
156
+ command?: string;
157
+ /** The directory in which the command is executed. */
158
+ cwd?: string;
159
+ /** The directory to watch for changes while using wrangler dev, defaults to the current working directory */
160
+ watch_dir?: string;
161
+ /**
162
+ * Deprecated field previously used to configure the build and upload of the script.
163
+ * @deprecated
164
+ */
165
+ upload?: DeprecatedUpload;
166
+ };
167
+
136
168
  /**
137
169
  * TODO: remove this as it has been deprecated.
138
170
  *
@@ -277,6 +309,40 @@ interface EnvironmentDeprecated {
277
309
  }[];
278
310
  }
279
311
 
312
+ /**
313
+ * Deprecated upload configuration.
314
+ */
315
+ export interface DeprecatedUpload {
316
+ /**
317
+ * The format of the Worker script.
318
+ *
319
+ * @deprecated We infer the format automatically now.
320
+ */
321
+ format?: "modules" | "service-worker";
322
+
323
+ /**
324
+ * The directory you wish to upload your worker from,
325
+ * relative to the wrangler.toml file.
326
+ *
327
+ * Defaults to the directory containing the wrangler.toml file.
328
+ *
329
+ * @deprecated
330
+ */
331
+ dir?: string;
332
+
333
+ /**
334
+ * The path to the Worker script, relative to `upload.dir`.
335
+ *
336
+ * @deprecated This will be replaced by a command line argument.
337
+ */
338
+ main?: string;
339
+
340
+ /**
341
+ * @deprecated This is now defined at the top level `rules` field.
342
+ */
343
+ rules?: Environment["rules"];
344
+ }
345
+
280
346
  /**
281
347
  * The raw environment configuration that we read from the config file.
282
348
  *
@@ -3,7 +3,13 @@ import { parseTOML, readFileSync } from "../parse";
3
3
  import { normalizeAndValidateConfig } from "./validation";
4
4
  import type { Config, RawConfig } from "./config";
5
5
 
6
- export type { Config, RawConfig } from "./config";
6
+ export type {
7
+ Config,
8
+ RawConfig,
9
+ ConfigFields,
10
+ DevConfig,
11
+ RawDevConfig,
12
+ } from "./config";
7
13
  export type {
8
14
  Environment,
9
15
  RawEnvironment,
@@ -13,7 +19,10 @@ export type {
13
19
  /**
14
20
  * Get the Wrangler configuration; read it from the give `configPath` if available.
15
21
  */
16
- export function readConfig(configPath?: string): Config {
22
+ export function readConfig(
23
+ configPath: string | undefined,
24
+ args: unknown
25
+ ): Config {
17
26
  let rawConfig: RawConfig = {};
18
27
  if (!configPath) {
19
28
  configPath = findWranglerToml();
@@ -27,7 +36,8 @@ export function readConfig(configPath?: string): Config {
27
36
  // Process the top-level configuration.
28
37
  const { config, diagnostics } = normalizeAndValidateConfig(
29
38
  rawConfig,
30
- configPath
39
+ configPath,
40
+ args
31
41
  );
32
42
 
33
43
  if (diagnostics.hasWarnings()) {
@@ -1,11 +1,11 @@
1
- import type { Config, RawConfig } from "./config";
1
+ import type { RawConfig } from "./config";
2
2
  import type { Diagnostics } from "./diagnostics";
3
3
  import type { Environment, RawEnvironment } from "./environment";
4
4
 
5
5
  /**
6
6
  * Mark a field as deprecated.
7
7
  *
8
- * This function will add a diagnostics warning if the deprecated field is found in the `rawEnv`.
8
+ * This function will add a diagnostics warning if the deprecated field is found in the `rawEnv` (or an error if it's also a breaking deprecation)
9
9
  * The `fieldPath` is a dot separated property path, e.g. `"build.upload.format"`.
10
10
  */
11
11
  export function deprecated<T extends object>(
@@ -13,11 +13,14 @@ export function deprecated<T extends object>(
13
13
  config: T,
14
14
  fieldPath: DeepKeyOf<T>,
15
15
  message: string,
16
- remove: boolean
16
+ remove: boolean,
17
+ breaking = false
17
18
  ): void {
18
19
  const result = unwindPropertyPath(config, fieldPath);
19
20
  if (result !== undefined && result.field in result.container) {
20
- diagnostics.warnings.push(`DEPRECATION: "${fieldPath}":\n${message}`);
21
+ (breaking ? diagnostics.errors : diagnostics.warnings).push(
22
+ `DEPRECATION: "${fieldPath}":\n${message}`
23
+ );
21
24
  if (remove) {
22
25
  delete (result.container as Record<string, unknown>)[result.field];
23
26
  }
@@ -51,14 +54,82 @@ export function experimental<T extends object>(
51
54
  */
52
55
  export function inheritable<K extends keyof Environment>(
53
56
  diagnostics: Diagnostics,
54
- config: Config | undefined,
57
+ topLevelEnv: Environment | undefined,
58
+ rawEnv: RawEnvironment,
59
+ field: K,
60
+ validate: ValidatorFn,
61
+ defaultValue: Environment[K],
62
+ transformFn: TransformFn<Environment[K]> = (v) => v
63
+ ): Environment[K] {
64
+ validate(diagnostics, field, rawEnv[field], topLevelEnv);
65
+ return (
66
+ (rawEnv[field] as Environment[K]) ??
67
+ transformFn(topLevelEnv?.[field]) ??
68
+ defaultValue
69
+ );
70
+ }
71
+
72
+ /**
73
+ * Get an inheritable environment field, but only if we are in legacy environments
74
+ */
75
+ export function inheritableInLegacyEnvironments<K extends keyof Environment>(
76
+ diagnostics: Diagnostics,
77
+ isLegacyEnv: boolean | undefined,
78
+ topLevelEnv: Environment | undefined,
55
79
  rawEnv: RawEnvironment,
56
80
  field: K,
57
81
  validate: ValidatorFn,
82
+ transformFn: TransformFn<Environment[K]>,
58
83
  defaultValue: Environment[K]
59
84
  ): Environment[K] {
60
- validate(diagnostics, field, rawEnv[field], config);
61
- return (rawEnv[field] as Environment[K]) ?? config?.[field] ?? defaultValue;
85
+ return topLevelEnv === undefined || isLegacyEnv === true
86
+ ? inheritable(
87
+ diagnostics,
88
+ topLevelEnv,
89
+ rawEnv,
90
+ field,
91
+ validate,
92
+ defaultValue,
93
+ transformFn
94
+ )
95
+ : notAllowedInNamedServiceEnvironment(
96
+ diagnostics,
97
+ topLevelEnv,
98
+ rawEnv,
99
+ field
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Type of function that is used to transform an inheritable environment field.
105
+ */
106
+ type TransformFn<T> = (fieldValue: T | undefined) => T | undefined;
107
+
108
+ /**
109
+ * Transform an environment field by appending current environment name to it.
110
+ */
111
+ export const appendEnvName =
112
+ (envName: string): TransformFn<string | undefined> =>
113
+ (fieldValue) =>
114
+ fieldValue ? `${fieldValue}-${envName}` : undefined;
115
+
116
+ /**
117
+ * Log an error if this named environment is trying to override the value in the top-level
118
+ * environment, which is not allow for this field.
119
+ */
120
+ function notAllowedInNamedServiceEnvironment<K extends keyof Environment>(
121
+ diagnostics: Diagnostics,
122
+ topLevelEnv: Environment,
123
+ rawEnv: RawEnvironment,
124
+ field: K
125
+ ): Environment[K] {
126
+ if (field in rawEnv) {
127
+ diagnostics.errors.push(
128
+ `The "${field}" field is not allowed in named service environments.\n` +
129
+ `Please remove the field from this environment.`
130
+ );
131
+ }
132
+ return topLevelEnv[field];
62
133
  }
63
134
 
64
135
  /**
@@ -69,7 +140,7 @@ export function inheritable<K extends keyof Environment>(
69
140
  */
70
141
  export function notInheritable<K extends keyof Environment>(
71
142
  diagnostics: Diagnostics,
72
- config: Config | undefined,
143
+ topLevelEnv: Environment | undefined,
73
144
  rawConfig: RawConfig | undefined,
74
145
  rawEnv: RawEnvironment,
75
146
  envName: string,
@@ -78,7 +149,7 @@ export function notInheritable<K extends keyof Environment>(
78
149
  defaultValue: Environment[K]
79
150
  ): Environment[K] {
80
151
  if (rawEnv[field] !== undefined) {
81
- validate(diagnostics, field, rawEnv[field], config);
152
+ validate(diagnostics, field, rawEnv[field], topLevelEnv);
82
153
  } else {
83
154
  if (rawConfig?.[field] !== undefined) {
84
155
  diagnostics.warnings.push(
@@ -136,7 +207,7 @@ export type ValidatorFn = (
136
207
  diagnostics: Diagnostics,
137
208
  field: string,
138
209
  value: unknown,
139
- config: Config | undefined
210
+ topLevelEnv: Environment | undefined
140
211
  ) => boolean;
141
212
 
142
213
  /**
@@ -145,7 +216,7 @@ export type ValidatorFn = (
145
216
  export const isString: ValidatorFn = (diagnostics, field, value) => {
146
217
  if (value !== undefined && typeof value !== "string") {
147
218
  diagnostics.errors.push(
148
- `Expected "${field}" field to be a string but got ${JSON.stringify(
219
+ `Expected "${field}" to be of type string but got ${JSON.stringify(
149
220
  value
150
221
  )}.`
151
222
  );
@@ -163,7 +234,7 @@ export const isStringArray: ValidatorFn = (diagnostics, field, value) => {
163
234
  (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
164
235
  ) {
165
236
  diagnostics.errors.push(
166
- `Expected "${field}" field to be an array of strings but got ${JSON.stringify(
237
+ `Expected "${field}" to be of type string array but got ${JSON.stringify(
167
238
  value
168
239
  )}.`
169
240
  );
@@ -185,7 +256,7 @@ export const isObjectWith =
185
256
  !properties.every((prop) => prop in value))
186
257
  ) {
187
258
  diagnostics.errors.push(
188
- `Expected "${field}" field to be an object containing properties ${properties} but got ${JSON.stringify(
259
+ `Expected "${field}" to be of type object, containing properties ${properties}, but got ${JSON.stringify(
189
260
  value
190
261
  )}.`
191
262
  );
@@ -202,22 +273,70 @@ export const isOneOf =
202
273
  (diagnostics, field, value) => {
203
274
  if (value !== undefined && !choices.some((choice) => value === choice)) {
204
275
  diagnostics.errors.push(
205
- `Expected "${field}" field to be on of ${choices} but got ${JSON.stringify(
206
- value
207
- )}.`
276
+ `Expected "${field}" field to be one of ${JSON.stringify(
277
+ choices
278
+ )} but got ${JSON.stringify(value)}.`
208
279
  );
209
280
  return false;
210
281
  }
211
282
  return true;
212
283
  };
213
284
 
285
+ /**
286
+ * Aggregate multiple validator functions
287
+ */
288
+ export const all = (...validations: ValidatorFn[]): ValidatorFn => {
289
+ return (diagnostics, field, value, config) => {
290
+ let passedValidations = true;
291
+
292
+ for (const validate of validations) {
293
+ if (!validate(diagnostics, field, value, config)) {
294
+ passedValidations = false;
295
+ }
296
+ }
297
+
298
+ return passedValidations;
299
+ };
300
+ };
301
+
302
+ /**
303
+ * Check that the field is mutually exclusive with a list of other fields.
304
+ *
305
+ * @param container the container of the fields to check against.
306
+ * @param fields the names of the fields to check against.
307
+ */
308
+ export const isMutuallyExclusiveWith = <T extends RawEnvironment | RawConfig>(
309
+ container: T,
310
+ ...fields: (keyof T)[]
311
+ ): ValidatorFn => {
312
+ return (diagnostics, field, value) => {
313
+ if (value === undefined) {
314
+ return true;
315
+ }
316
+
317
+ for (const exclusiveWith of fields) {
318
+ if (container[exclusiveWith] !== undefined) {
319
+ diagnostics.errors.push(
320
+ `Expected exactly one of the following fields ${JSON.stringify([
321
+ field,
322
+ ...fields,
323
+ ])}.`
324
+ );
325
+ return false;
326
+ }
327
+ }
328
+
329
+ return true;
330
+ };
331
+ };
332
+
214
333
  /**
215
334
  * Validate that the field is a boolean.
216
335
  */
217
336
  export const isBoolean: ValidatorFn = (diagnostics, field, value) => {
218
337
  if (value !== undefined && typeof value !== "boolean") {
219
338
  diagnostics.errors.push(
220
- `Expected "${field}" field to be a boolean but got ${JSON.stringify(
339
+ `Expected "${field}" to be of type boolean but got ${JSON.stringify(
221
340
  value
222
341
  )}.`
223
342
  );
@@ -234,21 +353,28 @@ export const validateRequiredProperty = (
234
353
  container: string,
235
354
  key: string,
236
355
  value: unknown,
237
- type: string
356
+ type: string,
357
+ choices?: unknown[]
238
358
  ): boolean => {
239
359
  if (container) {
240
360
  container += ".";
241
361
  }
242
362
  if (value === undefined) {
243
- diagnostics.errors.push(`ERROR: "${container}${key}" is a required field.`);
363
+ diagnostics.errors.push(`"${container}${key}" is a required field.`);
244
364
  return false;
245
365
  } else if (typeof value !== type) {
246
366
  diagnostics.errors.push(
247
- `ERROR: "${container}${key}" should be of type ${type} but got ${JSON.stringify(
367
+ `Expected "${container}${key}" to be of type ${type} but got ${JSON.stringify(
248
368
  value
249
369
  )}.`
250
370
  );
251
371
  return false;
372
+ } else if (choices) {
373
+ if (
374
+ !isOneOf(...choices)(diagnostics, `${container}${key}`, value, undefined)
375
+ ) {
376
+ return false;
377
+ }
252
378
  }
253
379
  return true;
254
380
  };
@@ -261,10 +387,18 @@ export const validateOptionalProperty = (
261
387
  container: string,
262
388
  key: string,
263
389
  value: unknown,
264
- type: string
390
+ type: string,
391
+ choices?: unknown[]
265
392
  ): boolean => {
266
393
  if (value !== undefined) {
267
- return validateRequiredProperty(diagnostics, container, key, value, type);
394
+ return validateRequiredProperty(
395
+ diagnostics,
396
+ container,
397
+ key,
398
+ value,
399
+ type,
400
+ choices
401
+ );
268
402
  }
269
403
  return true;
270
404
  };
@@ -281,7 +415,7 @@ export const validateTypedArray = (
281
415
  let isValid = true;
282
416
  if (!Array.isArray(value)) {
283
417
  diagnostics.errors.push(
284
- `"${container}" should be an array of ${type}s but got ${JSON.stringify(
418
+ `Expected "${container}" to be an array of ${type}s but got ${JSON.stringify(
285
419
  value
286
420
  )}`
287
421
  );
@@ -368,3 +502,53 @@ export const validateAdditionalProperties = (
368
502
  }
369
503
  return true;
370
504
  };
505
+
506
+ /**
507
+ * Get the names of the bindings collection in `value`.
508
+ *
509
+ * Will return an empty array if it doesn't understand the value
510
+ * passed in, so another form of validation should be
511
+ * performed externally.
512
+ */
513
+ export const getBindingNames = (value: unknown): string[] => {
514
+ if (typeof value !== "object" || value === null) {
515
+ return [];
516
+ }
517
+
518
+ if (isBindingList(value)) {
519
+ return value.bindings.map(({ name }) => name);
520
+ } else if (isNamespaceList(value)) {
521
+ return value.map(({ binding }) => binding);
522
+ } else if (isRecord(value)) {
523
+ return Object.keys(value);
524
+ } else {
525
+ return [];
526
+ }
527
+ };
528
+
529
+ const isBindingList = (
530
+ value: unknown
531
+ ): value is {
532
+ bindings: {
533
+ name: string;
534
+ }[];
535
+ } =>
536
+ isRecord(value) &&
537
+ "bindings" in value &&
538
+ Array.isArray(value.bindings) &&
539
+ value.bindings.every(
540
+ (binding) =>
541
+ isRecord(binding) && "name" in binding && typeof binding.name === "string"
542
+ );
543
+
544
+ const isNamespaceList = (value: unknown): value is { binding: string }[] =>
545
+ Array.isArray(value) &&
546
+ value.every(
547
+ (entry) =>
548
+ isRecord(entry) && "binding" in entry && typeof entry.binding === "string"
549
+ );
550
+
551
+ const isRecord = (
552
+ value: unknown
553
+ ): value is Record<string | number | symbol, unknown> =>
554
+ typeof value === "object" && value !== null && !Array.isArray(value);