wrangler 0.0.0-e043479 → 0.0.0-e0a0509

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 (96) hide show
  1. package/README.md +27 -14
  2. package/bin/wrangler.js +13 -1
  3. package/miniflare-dist/index.mjs +4886 -0
  4. package/package.json +38 -25
  5. package/pages/functions/buildPlugin.ts +92 -0
  6. package/pages/functions/buildWorker.ts +80 -4
  7. package/pages/functions/filepath-routing.test.ts +136 -25
  8. package/pages/functions/filepath-routing.ts +14 -3
  9. package/pages/functions/routes.ts +6 -1
  10. package/pages/functions/template-plugin.ts +146 -0
  11. package/pages/functions/template-worker.ts +21 -8
  12. package/src/__tests__/config-cache.test.ts +36 -0
  13. package/src/__tests__/configuration.test.ts +1294 -355
  14. package/src/__tests__/dev.test.tsx +592 -12
  15. package/src/__tests__/guess-worker-format.test.ts +37 -18
  16. package/src/__tests__/helpers/mock-account-id.ts +4 -0
  17. package/src/__tests__/helpers/mock-cfetch.ts +8 -4
  18. package/src/__tests__/helpers/mock-console.ts +20 -3
  19. package/src/__tests__/helpers/mock-dialogs.ts +26 -1
  20. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  21. package/src/__tests__/helpers/mock-istty.ts +27 -0
  22. package/src/__tests__/helpers/mock-kv.ts +9 -16
  23. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  24. package/src/__tests__/helpers/run-in-tmp.ts +10 -0
  25. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  26. package/src/__tests__/https-options.test.ts +130 -0
  27. package/src/__tests__/index.test.ts +129 -753
  28. package/src/__tests__/init.test.ts +1516 -0
  29. package/src/__tests__/jest.setup.ts +13 -5
  30. package/src/__tests__/kv.test.ts +208 -174
  31. package/src/__tests__/logger.test.ts +115 -0
  32. package/src/__tests__/pages.test.ts +327 -0
  33. package/src/__tests__/parse.test.ts +4 -0
  34. package/src/__tests__/publish.test.ts +3313 -505
  35. package/src/__tests__/r2.test.ts +38 -26
  36. package/src/__tests__/route.test.ts +45 -0
  37. package/src/__tests__/secret.test.ts +55 -34
  38. package/src/__tests__/tail.test.ts +146 -20
  39. package/src/__tests__/user.test.ts +131 -0
  40. package/src/__tests__/whoami.test.tsx +26 -7
  41. package/src/bundle.ts +43 -7
  42. package/src/cfetch/internal.ts +9 -5
  43. package/src/cli.ts +5 -11
  44. package/src/config/README.md +7 -7
  45. package/src/config/config.ts +69 -105
  46. package/src/config/environment.ts +93 -4
  47. package/src/config/index.ts +15 -4
  48. package/src/config/validation-helpers.ts +228 -23
  49. package/src/config/validation.ts +559 -154
  50. package/src/config-cache.ts +45 -0
  51. package/src/{api/preview.ts → create-worker-preview.ts} +41 -13
  52. package/src/{api/form_data.ts → create-worker-upload-form.ts} +33 -10
  53. package/src/dev/dev-vars.ts +36 -0
  54. package/src/dev/dev.tsx +418 -0
  55. package/src/dev/local.tsx +312 -0
  56. package/src/dev/remote.tsx +213 -0
  57. package/src/dev/use-esbuild.ts +118 -0
  58. package/src/dialogs.tsx +16 -6
  59. package/src/entry.ts +222 -12
  60. package/src/environment-variables.ts +3 -1
  61. package/src/errors.ts +11 -0
  62. package/src/https-options.ts +123 -0
  63. package/src/index.tsx +878 -482
  64. package/src/inspect.ts +25 -11
  65. package/src/intl-polyfill.d.ts +139 -0
  66. package/src/{kv.tsx → kv.ts} +7 -27
  67. package/src/logger.ts +66 -0
  68. package/src/miniflare-cli/README.md +30 -0
  69. package/src/miniflare-cli/enum-keys.ts +17 -0
  70. package/src/miniflare-cli/index.ts +41 -0
  71. package/src/miniflare-cli/request-context.ts +40 -0
  72. package/src/module-collection.ts +94 -6
  73. package/src/open-in-browser.ts +18 -5
  74. package/src/package-manager.ts +9 -8
  75. package/src/pages.tsx +892 -62
  76. package/src/parse.ts +4 -2
  77. package/src/proxy.ts +128 -55
  78. package/src/publish.ts +305 -199
  79. package/src/selfsigned.d.ts +29 -0
  80. package/src/sites.tsx +88 -33
  81. package/src/tail/index.ts +2 -2
  82. package/src/tail/printing.ts +13 -8
  83. package/src/update-check.ts +19 -0
  84. package/src/user.tsx +244 -104
  85. package/src/whoami.tsx +2 -1
  86. package/src/{api/worker.ts → worker.ts} +15 -29
  87. package/templates/gitignore +171 -0
  88. package/templates/static-asset-facade.js +1 -1
  89. package/wrangler-dist/cli.js +23568 -50489
  90. package/src/__tests__/helpers/mock-user.ts +0 -27
  91. package/src/__tests__/logout.test.ts +0 -50
  92. package/src/__tests__/sentry.test.ts +0 -146
  93. package/src/dev.tsx +0 -960
  94. package/src/guess-worker-format.ts +0 -68
  95. package/src/reporting.ts +0 -115
  96. package/wrangler-dist/cli.js.map +0 -7
@@ -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.
@@ -69,7 +74,13 @@ interface EnvironmentInheritable {
69
74
  *
70
75
  * @inheritable
71
76
  */
72
- routes: string[] | undefined;
77
+ routes:
78
+ | (
79
+ | string
80
+ | { pattern: string; zone_id: string }
81
+ | { pattern: string; zone_name: string }
82
+ )[]
83
+ | undefined;
73
84
 
74
85
  /**
75
86
  * A route that your worker should be published to. Literally
@@ -80,7 +91,18 @@ interface EnvironmentInheritable {
80
91
  *
81
92
  * @inheritable
82
93
  */
83
- route: string | undefined;
94
+ route:
95
+ | (
96
+ | string
97
+ | { pattern: string; zone_id: string }
98
+ | { pattern: string; zone_name: string }
99
+ )
100
+ | undefined;
101
+
102
+ /**
103
+ * Path to a custom tsconfig
104
+ */
105
+ tsconfig: string | undefined;
84
106
 
85
107
  /**
86
108
  * The function to use to replace jsx syntax.
@@ -121,7 +143,7 @@ interface EnvironmentInheritable {
121
143
  *
122
144
  * @inheritable
123
145
  */
124
- usage_model: undefined | "bundled" | "unbound";
146
+ usage_model: "bundled" | "unbound" | undefined;
125
147
 
126
148
  /**
127
149
  * An ordered list of rules that define which modules to import,
@@ -133,6 +155,39 @@ interface EnvironmentInheritable {
133
155
  */
134
156
  rules: Rule[];
135
157
 
158
+ /**
159
+ * Configures a custom build step to be run by Wrangler when building your Worker.
160
+ *
161
+ * Refer to the [custom builds documentation](https://developers.cloudflare.com/workers/cli-wrangler/configuration#build)
162
+ * for more details.
163
+ *
164
+ * @default {}
165
+ */
166
+ build: {
167
+ /** 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. */
168
+ command?: string;
169
+ /** The directory in which the command is executed. */
170
+ cwd?: string;
171
+ /** The directory to watch for changes while using wrangler dev, defaults to the current working directory */
172
+ watch_dir?: string;
173
+ /**
174
+ * Deprecated field previously used to configure the build and upload of the script.
175
+ * @deprecated
176
+ */
177
+ upload?: DeprecatedUpload;
178
+ };
179
+
180
+ /**
181
+ * Minify the script before uploading.
182
+ * @inheritable
183
+ */
184
+ minify: boolean | undefined;
185
+
186
+ /**
187
+ * Add polyfills for node builtin modules and globals
188
+ */
189
+ node_compat: boolean | undefined;
190
+
136
191
  /**
137
192
  * TODO: remove this as it has been deprecated.
138
193
  *
@@ -277,6 +332,40 @@ interface EnvironmentDeprecated {
277
332
  }[];
278
333
  }
279
334
 
335
+ /**
336
+ * Deprecated upload configuration.
337
+ */
338
+ export interface DeprecatedUpload {
339
+ /**
340
+ * The format of the Worker script.
341
+ *
342
+ * @deprecated We infer the format automatically now.
343
+ */
344
+ format?: "modules" | "service-worker";
345
+
346
+ /**
347
+ * The directory you wish to upload your worker from,
348
+ * relative to the wrangler.toml file.
349
+ *
350
+ * Defaults to the directory containing the wrangler.toml file.
351
+ *
352
+ * @deprecated
353
+ */
354
+ dir?: string;
355
+
356
+ /**
357
+ * The path to the Worker script, relative to `upload.dir`.
358
+ *
359
+ * @deprecated This will be replaced by a command line argument.
360
+ */
361
+ main?: string;
362
+
363
+ /**
364
+ * @deprecated This is now defined at the top level `rules` field.
365
+ */
366
+ rules?: Environment["rules"];
367
+ }
368
+
280
369
  /**
281
370
  * The raw environment configuration that we read from the config file.
282
371
  *
@@ -1,9 +1,16 @@
1
1
  import { findUpSync } from "find-up";
2
+ import { logger } from "../logger";
2
3
  import { parseTOML, readFileSync } from "../parse";
3
4
  import { normalizeAndValidateConfig } from "./validation";
4
5
  import type { Config, RawConfig } from "./config";
5
6
 
6
- export type { Config, RawConfig } from "./config";
7
+ export type {
8
+ Config,
9
+ RawConfig,
10
+ ConfigFields,
11
+ DevConfig,
12
+ RawDevConfig,
13
+ } from "./config";
7
14
  export type {
8
15
  Environment,
9
16
  RawEnvironment,
@@ -13,7 +20,10 @@ export type {
13
20
  /**
14
21
  * Get the Wrangler configuration; read it from the give `configPath` if available.
15
22
  */
16
- export function readConfig(configPath?: string): Config {
23
+ export function readConfig(
24
+ configPath: string | undefined,
25
+ args: unknown
26
+ ): Config {
17
27
  let rawConfig: RawConfig = {};
18
28
  if (!configPath) {
19
29
  configPath = findWranglerToml();
@@ -27,11 +37,12 @@ export function readConfig(configPath?: string): Config {
27
37
  // Process the top-level configuration.
28
38
  const { config, diagnostics } = normalizeAndValidateConfig(
29
39
  rawConfig,
30
- configPath
40
+ configPath,
41
+ args
31
42
  );
32
43
 
33
44
  if (diagnostics.hasWarnings()) {
34
- console.warn(diagnostics.renderWarnings());
45
+ logger.warn(diagnostics.renderWarnings());
35
46
  }
36
47
  if (diagnostics.hasErrors()) {
37
48
  throw new Error(diagnostics.renderErrors());
@@ -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,16 @@ 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
+ title = "Deprecation",
18
+ type: "warning" | "error" = "warning"
17
19
  ): void {
20
+ const BOLD = "\x1b[1m";
21
+ const NORMAL = "\x1b[0m";
22
+ const diagnosticMessage = `${BOLD}${title}${NORMAL}: "${fieldPath}":\n${message}`;
18
23
  const result = unwindPropertyPath(config, fieldPath);
19
24
  if (result !== undefined && result.field in result.container) {
20
- diagnostics.warnings.push(`DEPRECATION: "${fieldPath}":\n${message}`);
25
+ diagnostics[`${type}s`].push(diagnosticMessage);
21
26
  if (remove) {
22
27
  delete (result.container as Record<string, unknown>)[result.field];
23
28
  }
@@ -51,14 +56,82 @@ export function experimental<T extends object>(
51
56
  */
52
57
  export function inheritable<K extends keyof Environment>(
53
58
  diagnostics: Diagnostics,
54
- config: Config | undefined,
59
+ topLevelEnv: Environment | undefined,
55
60
  rawEnv: RawEnvironment,
56
61
  field: K,
57
62
  validate: ValidatorFn,
63
+ defaultValue: Environment[K],
64
+ transformFn: TransformFn<Environment[K]> = (v) => v
65
+ ): Environment[K] {
66
+ validate(diagnostics, field, rawEnv[field], topLevelEnv);
67
+ return (
68
+ (rawEnv[field] as Environment[K]) ??
69
+ transformFn(topLevelEnv?.[field]) ??
70
+ defaultValue
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Get an inheritable environment field, but only if we are in legacy environments
76
+ */
77
+ export function inheritableInLegacyEnvironments<K extends keyof Environment>(
78
+ diagnostics: Diagnostics,
79
+ isLegacyEnv: boolean | undefined,
80
+ topLevelEnv: Environment | undefined,
81
+ rawEnv: RawEnvironment,
82
+ field: K,
83
+ validate: ValidatorFn,
84
+ transformFn: TransformFn<Environment[K]> = (v) => v,
58
85
  defaultValue: Environment[K]
59
86
  ): Environment[K] {
60
- validate(diagnostics, field, rawEnv[field], config);
61
- return (rawEnv[field] as Environment[K]) ?? config?.[field] ?? defaultValue;
87
+ return topLevelEnv === undefined || isLegacyEnv === true
88
+ ? inheritable(
89
+ diagnostics,
90
+ topLevelEnv,
91
+ rawEnv,
92
+ field,
93
+ validate,
94
+ defaultValue,
95
+ transformFn
96
+ )
97
+ : notAllowedInNamedServiceEnvironment(
98
+ diagnostics,
99
+ topLevelEnv,
100
+ rawEnv,
101
+ field
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Type of function that is used to transform an inheritable environment field.
107
+ */
108
+ type TransformFn<T> = (fieldValue: T | undefined) => T | undefined;
109
+
110
+ /**
111
+ * Transform an environment field by appending current environment name to it.
112
+ */
113
+ export const appendEnvName =
114
+ (envName: string): TransformFn<string | undefined> =>
115
+ (fieldValue) =>
116
+ fieldValue ? `${fieldValue}-${envName}` : undefined;
117
+
118
+ /**
119
+ * Log an error if this named environment is trying to override the value in the top-level
120
+ * environment, which is not allow for this field.
121
+ */
122
+ function notAllowedInNamedServiceEnvironment<K extends keyof Environment>(
123
+ diagnostics: Diagnostics,
124
+ topLevelEnv: Environment,
125
+ rawEnv: RawEnvironment,
126
+ field: K
127
+ ): Environment[K] {
128
+ if (field in rawEnv) {
129
+ diagnostics.errors.push(
130
+ `The "${field}" field is not allowed in named service environments.\n` +
131
+ `Please remove the field from this environment.`
132
+ );
133
+ }
134
+ return topLevelEnv[field];
62
135
  }
63
136
 
64
137
  /**
@@ -69,7 +142,7 @@ export function inheritable<K extends keyof Environment>(
69
142
  */
70
143
  export function notInheritable<K extends keyof Environment>(
71
144
  diagnostics: Diagnostics,
72
- config: Config | undefined,
145
+ topLevelEnv: Environment | undefined,
73
146
  rawConfig: RawConfig | undefined,
74
147
  rawEnv: RawEnvironment,
75
148
  envName: string,
@@ -78,7 +151,7 @@ export function notInheritable<K extends keyof Environment>(
78
151
  defaultValue: Environment[K]
79
152
  ): Environment[K] {
80
153
  if (rawEnv[field] !== undefined) {
81
- validate(diagnostics, field, rawEnv[field], config);
154
+ validate(diagnostics, field, rawEnv[field], topLevelEnv);
82
155
  } else {
83
156
  if (rawConfig?.[field] !== undefined) {
84
157
  diagnostics.warnings.push(
@@ -136,7 +209,7 @@ export type ValidatorFn = (
136
209
  diagnostics: Diagnostics,
137
210
  field: string,
138
211
  value: unknown,
139
- config: Config | undefined
212
+ topLevelEnv: Environment | undefined
140
213
  ) => boolean;
141
214
 
142
215
  /**
@@ -145,7 +218,7 @@ export type ValidatorFn = (
145
218
  export const isString: ValidatorFn = (diagnostics, field, value) => {
146
219
  if (value !== undefined && typeof value !== "string") {
147
220
  diagnostics.errors.push(
148
- `Expected "${field}" field to be a string but got ${JSON.stringify(
221
+ `Expected "${field}" to be of type string but got ${JSON.stringify(
149
222
  value
150
223
  )}.`
151
224
  );
@@ -154,6 +227,25 @@ export const isString: ValidatorFn = (diagnostics, field, value) => {
154
227
  return true;
155
228
  };
156
229
 
230
+ /**
231
+ * Validate that the `name` field is compliant with EWC constraints.
232
+ */
233
+ export const isValidName: ValidatorFn = (diagnostics, field, value) => {
234
+ if (
235
+ (typeof value === "string" && /^$|^[a-z0-9_ ][a-z0-9-_ ]*$/.test(value)) ||
236
+ value === undefined
237
+ ) {
238
+ return true;
239
+ } else {
240
+ diagnostics.errors.push(
241
+ `Expected "${field}" to be of type string, alphanumeric and lowercase with dashes only but got ${JSON.stringify(
242
+ value
243
+ )}.`
244
+ );
245
+ return false;
246
+ }
247
+ };
248
+
157
249
  /**
158
250
  * Validate that the field is an array of strings.
159
251
  */
@@ -163,7 +255,7 @@ export const isStringArray: ValidatorFn = (diagnostics, field, value) => {
163
255
  (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
164
256
  ) {
165
257
  diagnostics.errors.push(
166
- `Expected "${field}" field to be an array of strings but got ${JSON.stringify(
258
+ `Expected "${field}" to be of type string array but got ${JSON.stringify(
167
259
  value
168
260
  )}.`
169
261
  );
@@ -185,7 +277,7 @@ export const isObjectWith =
185
277
  !properties.every((prop) => prop in value))
186
278
  ) {
187
279
  diagnostics.errors.push(
188
- `Expected "${field}" field to be an object containing properties ${properties} but got ${JSON.stringify(
280
+ `Expected "${field}" to be of type object, containing properties ${properties}, but got ${JSON.stringify(
189
281
  value
190
282
  )}.`
191
283
  );
@@ -202,22 +294,70 @@ export const isOneOf =
202
294
  (diagnostics, field, value) => {
203
295
  if (value !== undefined && !choices.some((choice) => value === choice)) {
204
296
  diagnostics.errors.push(
205
- `Expected "${field}" field to be on of ${choices} but got ${JSON.stringify(
206
- value
207
- )}.`
297
+ `Expected "${field}" field to be one of ${JSON.stringify(
298
+ choices
299
+ )} but got ${JSON.stringify(value)}.`
208
300
  );
209
301
  return false;
210
302
  }
211
303
  return true;
212
304
  };
213
305
 
306
+ /**
307
+ * Aggregate multiple validator functions
308
+ */
309
+ export const all = (...validations: ValidatorFn[]): ValidatorFn => {
310
+ return (diagnostics, field, value, config) => {
311
+ let passedValidations = true;
312
+
313
+ for (const validate of validations) {
314
+ if (!validate(diagnostics, field, value, config)) {
315
+ passedValidations = false;
316
+ }
317
+ }
318
+
319
+ return passedValidations;
320
+ };
321
+ };
322
+
323
+ /**
324
+ * Check that the field is mutually exclusive with a list of other fields.
325
+ *
326
+ * @param container the container of the fields to check against.
327
+ * @param fields the names of the fields to check against.
328
+ */
329
+ export const isMutuallyExclusiveWith = <T extends RawEnvironment | RawConfig>(
330
+ container: T,
331
+ ...fields: (keyof T)[]
332
+ ): ValidatorFn => {
333
+ return (diagnostics, field, value) => {
334
+ if (value === undefined) {
335
+ return true;
336
+ }
337
+
338
+ for (const exclusiveWith of fields) {
339
+ if (container[exclusiveWith] !== undefined) {
340
+ diagnostics.errors.push(
341
+ `Expected exactly one of the following fields ${JSON.stringify([
342
+ field,
343
+ ...fields,
344
+ ])}.`
345
+ );
346
+ return false;
347
+ }
348
+ }
349
+
350
+ return true;
351
+ };
352
+ };
353
+
214
354
  /**
215
355
  * Validate that the field is a boolean.
216
356
  */
217
357
  export const isBoolean: ValidatorFn = (diagnostics, field, value) => {
218
358
  if (value !== undefined && typeof value !== "boolean") {
219
359
  diagnostics.errors.push(
220
- `Expected "${field}" field to be a boolean but got ${JSON.stringify(
360
+ `Expected "${field}" to be of type boolean but got ${JSON.stringify(
221
361
  value
222
362
  )}.`
223
363
  );
@@ -234,21 +374,28 @@ export const validateRequiredProperty = (
234
374
  container: string,
235
375
  key: string,
236
376
  value: unknown,
237
- type: string
377
+ type: string,
378
+ choices?: unknown[]
238
379
  ): boolean => {
239
380
  if (container) {
240
381
  container += ".";
241
382
  }
242
383
  if (value === undefined) {
243
- diagnostics.errors.push(`ERROR: "${container}${key}" is a required field.`);
384
+ diagnostics.errors.push(`"${container}${key}" is a required field.`);
244
385
  return false;
245
386
  } else if (typeof value !== type) {
246
387
  diagnostics.errors.push(
247
- `ERROR: "${container}${key}" should be of type ${type} but got ${JSON.stringify(
388
+ `Expected "${container}${key}" to be of type ${type} but got ${JSON.stringify(
248
389
  value
249
390
  )}.`
250
391
  );
251
392
  return false;
393
+ } else if (choices) {
394
+ if (
395
+ !isOneOf(...choices)(diagnostics, `${container}${key}`, value, undefined)
396
+ ) {
397
+ return false;
398
+ }
252
399
  }
253
400
  return true;
254
401
  };
@@ -261,10 +408,18 @@ export const validateOptionalProperty = (
261
408
  container: string,
262
409
  key: string,
263
410
  value: unknown,
264
- type: string
411
+ type: string,
412
+ choices?: unknown[]
265
413
  ): boolean => {
266
414
  if (value !== undefined) {
267
- return validateRequiredProperty(diagnostics, container, key, value, type);
415
+ return validateRequiredProperty(
416
+ diagnostics,
417
+ container,
418
+ key,
419
+ value,
420
+ type,
421
+ choices
422
+ );
268
423
  }
269
424
  return true;
270
425
  };
@@ -281,7 +436,7 @@ export const validateTypedArray = (
281
436
  let isValid = true;
282
437
  if (!Array.isArray(value)) {
283
438
  diagnostics.errors.push(
284
- `"${container}" should be an array of ${type}s but got ${JSON.stringify(
439
+ `Expected "${container}" to be an array of ${type}s but got ${JSON.stringify(
285
440
  value
286
441
  )}`
287
442
  );
@@ -368,3 +523,53 @@ export const validateAdditionalProperties = (
368
523
  }
369
524
  return true;
370
525
  };
526
+
527
+ /**
528
+ * Get the names of the bindings collection in `value`.
529
+ *
530
+ * Will return an empty array if it doesn't understand the value
531
+ * passed in, so another form of validation should be
532
+ * performed externally.
533
+ */
534
+ export const getBindingNames = (value: unknown): string[] => {
535
+ if (typeof value !== "object" || value === null) {
536
+ return [];
537
+ }
538
+
539
+ if (isBindingList(value)) {
540
+ return value.bindings.map(({ name }) => name);
541
+ } else if (isNamespaceList(value)) {
542
+ return value.map(({ binding }) => binding);
543
+ } else if (isRecord(value)) {
544
+ return Object.keys(value);
545
+ } else {
546
+ return [];
547
+ }
548
+ };
549
+
550
+ const isBindingList = (
551
+ value: unknown
552
+ ): value is {
553
+ bindings: {
554
+ name: string;
555
+ }[];
556
+ } =>
557
+ isRecord(value) &&
558
+ "bindings" in value &&
559
+ Array.isArray(value.bindings) &&
560
+ value.bindings.every(
561
+ (binding) =>
562
+ isRecord(binding) && "name" in binding && typeof binding.name === "string"
563
+ );
564
+
565
+ const isNamespaceList = (value: unknown): value is { binding: string }[] =>
566
+ Array.isArray(value) &&
567
+ value.every(
568
+ (entry) =>
569
+ isRecord(entry) && "binding" in entry && typeof entry.binding === "string"
570
+ );
571
+
572
+ const isRecord = (
573
+ value: unknown
574
+ ): value is Record<string | number | symbol, unknown> =>
575
+ typeof value === "object" && value !== null && !Array.isArray(value);