wrangler 0.0.0-e3cab74 → 0.0.0-e3ce82b

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 (139) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +49 -5
  4. package/miniflare-dist/index.mjs +4886 -0
  5. package/package.json +43 -28
  6. package/pages/functions/buildPlugin.ts +105 -0
  7. package/pages/functions/buildWorker.ts +93 -4
  8. package/pages/functions/filepath-routing.test.ts +136 -25
  9. package/pages/functions/filepath-routing.ts +14 -3
  10. package/pages/functions/routes.ts +6 -1
  11. package/pages/functions/template-plugin.ts +147 -0
  12. package/pages/functions/template-worker.ts +21 -8
  13. package/src/__tests__/config-cache.test.ts +36 -0
  14. package/src/__tests__/configuration.test.ts +1713 -411
  15. package/src/__tests__/dev.test.tsx +954 -84
  16. package/src/__tests__/guess-worker-format.test.ts +37 -18
  17. package/src/__tests__/helpers/mock-account-id.ts +22 -4
  18. package/src/__tests__/helpers/mock-cfetch.ts +8 -4
  19. package/src/__tests__/helpers/mock-console.ts +35 -10
  20. package/src/__tests__/helpers/mock-dialogs.ts +67 -2
  21. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  22. package/src/__tests__/helpers/mock-istty.ts +27 -0
  23. package/src/__tests__/helpers/mock-kv.ts +9 -16
  24. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  25. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  26. package/src/__tests__/helpers/run-in-tmp.ts +10 -0
  27. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  28. package/src/__tests__/https-options.test.ts +130 -0
  29. package/src/__tests__/index.test.ts +138 -747
  30. package/src/__tests__/init.test.ts +1637 -0
  31. package/src/__tests__/jest.setup.ts +28 -8
  32. package/src/__tests__/kv.test.ts +598 -258
  33. package/src/__tests__/logger.test.ts +115 -0
  34. package/src/__tests__/package-manager.test.ts +154 -7
  35. package/src/__tests__/pages.test.ts +754 -0
  36. package/src/__tests__/parse.test.ts +8 -0
  37. package/src/__tests__/publish.test.ts +4374 -1025
  38. package/src/__tests__/r2.test.ts +69 -34
  39. package/src/__tests__/route.test.ts +45 -0
  40. package/src/__tests__/secret.test.ts +228 -73
  41. package/src/__tests__/tail.test.ts +146 -20
  42. package/src/__tests__/user.test.ts +131 -0
  43. package/src/__tests__/whoami.test.tsx +60 -7
  44. package/src/abort.d.ts +3 -0
  45. package/src/bundle.ts +75 -11
  46. package/src/cfetch/index.ts +21 -4
  47. package/src/cfetch/internal.ts +29 -20
  48. package/src/cli.ts +5 -11
  49. package/src/config/README.md +7 -7
  50. package/src/config/config.ts +69 -100
  51. package/src/config/environment.ts +119 -4
  52. package/src/config/index.ts +177 -4
  53. package/src/config/validation-helpers.ts +237 -23
  54. package/src/config/validation.ts +722 -191
  55. package/src/config-cache.ts +45 -0
  56. package/src/{api/preview.ts → create-worker-preview.ts} +65 -27
  57. package/src/{api/form_data.ts → create-worker-upload-form.ts} +55 -18
  58. package/src/dev/dev-vars.ts +36 -0
  59. package/src/dev/dev.tsx +401 -0
  60. package/src/dev/local.tsx +322 -0
  61. package/src/dev/remote.tsx +237 -0
  62. package/src/dev/use-esbuild.ts +118 -0
  63. package/src/dialogs.tsx +64 -6
  64. package/src/durable.ts +102 -0
  65. package/src/entry.ts +279 -14
  66. package/src/environment-variables.ts +3 -1
  67. package/src/errors.ts +11 -0
  68. package/src/https-options.ts +123 -0
  69. package/src/index.tsx +1386 -867
  70. package/src/inspect.ts +64 -11
  71. package/src/intl-polyfill.d.ts +139 -0
  72. package/src/{kv.tsx → kv.ts} +195 -62
  73. package/src/logger.ts +66 -0
  74. package/src/miniflare-cli/README.md +30 -0
  75. package/src/miniflare-cli/enum-keys.ts +17 -0
  76. package/src/miniflare-cli/index.ts +41 -0
  77. package/src/miniflare-cli/request-context.ts +40 -0
  78. package/src/module-collection.ts +94 -6
  79. package/src/open-in-browser.ts +9 -3
  80. package/src/package-manager.ts +59 -11
  81. package/src/pages.tsx +1123 -75
  82. package/src/parse.ts +26 -6
  83. package/src/proxy.ts +159 -57
  84. package/src/publish.ts +581 -228
  85. package/src/selfsigned.d.ts +29 -0
  86. package/src/sites.tsx +130 -45
  87. package/src/tail/index.ts +2 -2
  88. package/src/tail/printing.ts +13 -8
  89. package/src/update-check.ts +19 -0
  90. package/src/user.tsx +275 -110
  91. package/src/whoami.tsx +5 -3
  92. package/src/{api/worker.ts → worker.ts} +24 -29
  93. package/src/zones.ts +73 -0
  94. package/templates/gitignore +171 -0
  95. package/templates/new-worker-scheduled.js +17 -0
  96. package/templates/new-worker-scheduled.ts +32 -0
  97. package/templates/new-worker.ts +16 -1
  98. package/templates/static-asset-facade.js +1 -5
  99. package/wrangler-dist/cli.js +104803 -118375
  100. package/src/__tests__/helpers/mock-user.ts +0 -27
  101. package/src/__tests__/logout.test.ts +0 -50
  102. package/src/__tests__/sentry.test.ts +0 -146
  103. package/src/dev.tsx +0 -961
  104. package/src/guess-worker-format.ts +0 -68
  105. package/src/reporting.ts +0 -115
  106. package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
  107. package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
  108. package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
  109. package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
  110. package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
  111. package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
  112. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
  113. package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
  114. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
  115. package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
  116. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
  117. package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
  118. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
  119. package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
  120. package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
  121. package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
  122. package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
  123. package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
  124. package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
  125. package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
  126. package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
  127. package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
  128. package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
  129. package/vendor/wrangler-mime/CHANGELOG.md +0 -289
  130. package/vendor/wrangler-mime/LICENSE +0 -21
  131. package/vendor/wrangler-mime/Mime.js +0 -97
  132. package/vendor/wrangler-mime/README.md +0 -187
  133. package/vendor/wrangler-mime/cli.js +0 -46
  134. package/vendor/wrangler-mime/index.js +0 -4
  135. package/vendor/wrangler-mime/lite.js +0 -4
  136. package/vendor/wrangler-mime/package.json +0 -52
  137. package/vendor/wrangler-mime/types/other.js +0 -1
  138. package/vendor/wrangler-mime/types/standard.js +0 -1
  139. package/wrangler-dist/cli.js.map +0 -7
@@ -1,9 +1,17 @@
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";
5
+ import type { CfWorkerInit } from "../worker";
4
6
  import type { Config, RawConfig } from "./config";
5
7
 
6
- export type { Config, RawConfig } from "./config";
8
+ export type {
9
+ Config,
10
+ RawConfig,
11
+ ConfigFields,
12
+ DevConfig,
13
+ RawDevConfig,
14
+ } from "./config";
7
15
  export type {
8
16
  Environment,
9
17
  RawEnvironment,
@@ -13,7 +21,10 @@ export type {
13
21
  /**
14
22
  * Get the Wrangler configuration; read it from the give `configPath` if available.
15
23
  */
16
- export function readConfig(configPath?: string): Config {
24
+ export function readConfig(
25
+ configPath: string | undefined,
26
+ args: unknown
27
+ ): Config {
17
28
  let rawConfig: RawConfig = {};
18
29
  if (!configPath) {
19
30
  configPath = findWranglerToml();
@@ -27,11 +38,12 @@ export function readConfig(configPath?: string): Config {
27
38
  // Process the top-level configuration.
28
39
  const { config, diagnostics } = normalizeAndValidateConfig(
29
40
  rawConfig,
30
- configPath
41
+ configPath,
42
+ args
31
43
  );
32
44
 
33
45
  if (diagnostics.hasWarnings()) {
34
- console.warn(diagnostics.renderWarnings());
46
+ logger.warn(diagnostics.renderWarnings());
35
47
  }
36
48
  if (diagnostics.hasErrors()) {
37
49
  throw new Error(diagnostics.renderErrors());
@@ -50,3 +62,164 @@ export function findWranglerToml(
50
62
  const configPath = findUpSync("wrangler.toml", { cwd: referencePath });
51
63
  return configPath;
52
64
  }
65
+
66
+ /**
67
+ * Print all the bindings a worker using a given config would have access to
68
+ */
69
+ export function printBindings(bindings: CfWorkerInit["bindings"]) {
70
+ const truncate = (item: string | Record<string, unknown>) => {
71
+ const s = typeof item === "string" ? item : JSON.stringify(item);
72
+ const maxLength = 40;
73
+ if (s.length < maxLength) {
74
+ return s;
75
+ }
76
+
77
+ return `${s.substring(0, maxLength - 3)}...`;
78
+ };
79
+
80
+ const output: { type: string; entries: { key: string; value: string }[] }[] =
81
+ [];
82
+
83
+ const {
84
+ data_blobs,
85
+ durable_objects,
86
+ kv_namespaces,
87
+ r2_buckets,
88
+ services,
89
+ text_blobs,
90
+ unsafe,
91
+ vars,
92
+ wasm_modules,
93
+ } = bindings;
94
+
95
+ if (data_blobs !== undefined && Object.keys(data_blobs).length > 0) {
96
+ output.push({
97
+ type: "Data Blobs",
98
+ entries: Object.entries(data_blobs).map(([key, value]) => ({
99
+ key,
100
+ value: truncate(value),
101
+ })),
102
+ });
103
+ }
104
+
105
+ if (durable_objects !== undefined && durable_objects.bindings.length > 0) {
106
+ output.push({
107
+ type: "Durable Objects",
108
+ entries: durable_objects.bindings.map(
109
+ ({ name, class_name, script_name, environment }) => {
110
+ let value = class_name;
111
+ if (script_name) {
112
+ value += ` (defined in ${script_name})`;
113
+ }
114
+ if (environment) {
115
+ value += ` - ${environment}`;
116
+ }
117
+
118
+ return {
119
+ key: name,
120
+ value,
121
+ };
122
+ }
123
+ ),
124
+ });
125
+ }
126
+
127
+ if (kv_namespaces !== undefined && kv_namespaces.length > 0) {
128
+ output.push({
129
+ type: "KV Namespaces",
130
+ entries: kv_namespaces.map(({ binding, id }) => {
131
+ return {
132
+ key: binding,
133
+ value: id,
134
+ };
135
+ }),
136
+ });
137
+ }
138
+
139
+ if (r2_buckets !== undefined && r2_buckets.length > 0) {
140
+ output.push({
141
+ type: "R2 Buckets",
142
+ entries: r2_buckets.map(({ binding, bucket_name }) => {
143
+ return {
144
+ key: binding,
145
+ value: bucket_name,
146
+ };
147
+ }),
148
+ });
149
+ }
150
+
151
+ if (services !== undefined && services.length > 0) {
152
+ output.push({
153
+ type: "Services",
154
+ entries: services.map(({ binding, service, environment }) => {
155
+ let value = service;
156
+ if (environment) {
157
+ value += ` - ${environment}`;
158
+ }
159
+
160
+ return {
161
+ key: binding,
162
+ value,
163
+ };
164
+ }),
165
+ });
166
+ }
167
+
168
+ if (text_blobs !== undefined && Object.keys(text_blobs).length > 0) {
169
+ output.push({
170
+ type: "Text Blobs",
171
+ entries: Object.entries(text_blobs).map(([key, value]) => ({
172
+ key,
173
+ value: truncate(value),
174
+ })),
175
+ });
176
+ }
177
+
178
+ if (unsafe !== undefined && unsafe.length > 0) {
179
+ output.push({
180
+ type: "Unsafe",
181
+ entries: unsafe.map(({ name, type }) => ({
182
+ key: type,
183
+ value: name,
184
+ })),
185
+ });
186
+ }
187
+
188
+ if (vars !== undefined && Object.keys(vars).length > 0) {
189
+ output.push({
190
+ type: "Vars",
191
+ entries: Object.entries(vars).map(([key, value]) => ({
192
+ key,
193
+ value: `"${truncate(`${value}`)}"`,
194
+ })),
195
+ });
196
+ }
197
+
198
+ if (wasm_modules !== undefined && Object.keys(wasm_modules).length > 0) {
199
+ output.push({
200
+ type: "Wasm Modules",
201
+ entries: Object.entries(wasm_modules).map(([key, value]) => ({
202
+ key,
203
+ value: truncate(value),
204
+ })),
205
+ });
206
+ }
207
+
208
+ if (output.length === 0) {
209
+ return;
210
+ }
211
+
212
+ const message = [
213
+ `Your worker has access to the following bindings:`,
214
+ ...output
215
+ .map((bindingGroup) => {
216
+ return [
217
+ `- ${bindingGroup.type}:`,
218
+ bindingGroup.entries.map(({ key, value }) => ` - ${key}: ${value}`),
219
+ ];
220
+ })
221
+ .flat(2),
222
+ ].join("\n");
223
+
224
+ logger.log(message);
225
+ }
@@ -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,12 +277,21 @@ 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 only properties ${properties}, but got ${JSON.stringify(
189
281
  value
190
282
  )}.`
191
283
  );
192
284
  return false;
193
285
  }
286
+ // it's an object with the field as desired,
287
+ // but let's also check for unexpected fields
288
+ if (value !== undefined) {
289
+ const restFields = Object.keys(value).filter(
290
+ (key) => !properties.includes(key)
291
+ );
292
+ validateAdditionalProperties(diagnostics, field, restFields, []);
293
+ }
294
+
194
295
  return true;
195
296
  };
196
297
 
@@ -202,22 +303,70 @@ export const isOneOf =
202
303
  (diagnostics, field, value) => {
203
304
  if (value !== undefined && !choices.some((choice) => value === choice)) {
204
305
  diagnostics.errors.push(
205
- `Expected "${field}" field to be on of ${choices} but got ${JSON.stringify(
206
- value
207
- )}.`
306
+ `Expected "${field}" field to be one of ${JSON.stringify(
307
+ choices
308
+ )} but got ${JSON.stringify(value)}.`
208
309
  );
209
310
  return false;
210
311
  }
211
312
  return true;
212
313
  };
213
314
 
315
+ /**
316
+ * Aggregate multiple validator functions
317
+ */
318
+ export const all = (...validations: ValidatorFn[]): ValidatorFn => {
319
+ return (diagnostics, field, value, config) => {
320
+ let passedValidations = true;
321
+
322
+ for (const validate of validations) {
323
+ if (!validate(diagnostics, field, value, config)) {
324
+ passedValidations = false;
325
+ }
326
+ }
327
+
328
+ return passedValidations;
329
+ };
330
+ };
331
+
332
+ /**
333
+ * Check that the field is mutually exclusive with a list of other fields.
334
+ *
335
+ * @param container the container of the fields to check against.
336
+ * @param fields the names of the fields to check against.
337
+ */
338
+ export const isMutuallyExclusiveWith = <T extends RawEnvironment | RawConfig>(
339
+ container: T,
340
+ ...fields: (keyof T)[]
341
+ ): ValidatorFn => {
342
+ return (diagnostics, field, value) => {
343
+ if (value === undefined) {
344
+ return true;
345
+ }
346
+
347
+ for (const exclusiveWith of fields) {
348
+ if (container[exclusiveWith] !== undefined) {
349
+ diagnostics.errors.push(
350
+ `Expected exactly one of the following fields ${JSON.stringify([
351
+ field,
352
+ ...fields,
353
+ ])}.`
354
+ );
355
+ return false;
356
+ }
357
+ }
358
+
359
+ return true;
360
+ };
361
+ };
362
+
214
363
  /**
215
364
  * Validate that the field is a boolean.
216
365
  */
217
366
  export const isBoolean: ValidatorFn = (diagnostics, field, value) => {
218
367
  if (value !== undefined && typeof value !== "boolean") {
219
368
  diagnostics.errors.push(
220
- `Expected "${field}" field to be a boolean but got ${JSON.stringify(
369
+ `Expected "${field}" to be of type boolean but got ${JSON.stringify(
221
370
  value
222
371
  )}.`
223
372
  );
@@ -234,21 +383,28 @@ export const validateRequiredProperty = (
234
383
  container: string,
235
384
  key: string,
236
385
  value: unknown,
237
- type: string
386
+ type: string,
387
+ choices?: unknown[]
238
388
  ): boolean => {
239
389
  if (container) {
240
390
  container += ".";
241
391
  }
242
392
  if (value === undefined) {
243
- diagnostics.errors.push(`ERROR: "${container}${key}" is a required field.`);
393
+ diagnostics.errors.push(`"${container}${key}" is a required field.`);
244
394
  return false;
245
395
  } else if (typeof value !== type) {
246
396
  diagnostics.errors.push(
247
- `ERROR: "${container}${key}" should be of type ${type} but got ${JSON.stringify(
397
+ `Expected "${container}${key}" to be of type ${type} but got ${JSON.stringify(
248
398
  value
249
399
  )}.`
250
400
  );
251
401
  return false;
402
+ } else if (choices) {
403
+ if (
404
+ !isOneOf(...choices)(diagnostics, `${container}${key}`, value, undefined)
405
+ ) {
406
+ return false;
407
+ }
252
408
  }
253
409
  return true;
254
410
  };
@@ -261,10 +417,18 @@ export const validateOptionalProperty = (
261
417
  container: string,
262
418
  key: string,
263
419
  value: unknown,
264
- type: string
420
+ type: string,
421
+ choices?: unknown[]
265
422
  ): boolean => {
266
423
  if (value !== undefined) {
267
- return validateRequiredProperty(diagnostics, container, key, value, type);
424
+ return validateRequiredProperty(
425
+ diagnostics,
426
+ container,
427
+ key,
428
+ value,
429
+ type,
430
+ choices
431
+ );
268
432
  }
269
433
  return true;
270
434
  };
@@ -281,7 +445,7 @@ export const validateTypedArray = (
281
445
  let isValid = true;
282
446
  if (!Array.isArray(value)) {
283
447
  diagnostics.errors.push(
284
- `"${container}" should be an array of ${type}s but got ${JSON.stringify(
448
+ `Expected "${container}" to be an array of ${type}s but got ${JSON.stringify(
285
449
  value
286
450
  )}`
287
451
  );
@@ -368,3 +532,53 @@ export const validateAdditionalProperties = (
368
532
  }
369
533
  return true;
370
534
  };
535
+
536
+ /**
537
+ * Get the names of the bindings collection in `value`.
538
+ *
539
+ * Will return an empty array if it doesn't understand the value
540
+ * passed in, so another form of validation should be
541
+ * performed externally.
542
+ */
543
+ export const getBindingNames = (value: unknown): string[] => {
544
+ if (typeof value !== "object" || value === null) {
545
+ return [];
546
+ }
547
+
548
+ if (isBindingList(value)) {
549
+ return value.bindings.map(({ name }) => name);
550
+ } else if (isNamespaceList(value)) {
551
+ return value.map(({ binding }) => binding);
552
+ } else if (isRecord(value)) {
553
+ return Object.keys(value);
554
+ } else {
555
+ return [];
556
+ }
557
+ };
558
+
559
+ const isBindingList = (
560
+ value: unknown
561
+ ): value is {
562
+ bindings: {
563
+ name: string;
564
+ }[];
565
+ } =>
566
+ isRecord(value) &&
567
+ "bindings" in value &&
568
+ Array.isArray(value.bindings) &&
569
+ value.bindings.every(
570
+ (binding) =>
571
+ isRecord(binding) && "name" in binding && typeof binding.name === "string"
572
+ );
573
+
574
+ const isNamespaceList = (value: unknown): value is { binding: string }[] =>
575
+ Array.isArray(value) &&
576
+ value.every(
577
+ (entry) =>
578
+ isRecord(entry) && "binding" in entry && typeof entry.binding === "string"
579
+ );
580
+
581
+ const isRecord = (
582
+ value: unknown
583
+ ): value is Record<string | number | symbol, unknown> =>
584
+ typeof value === "object" && value !== null && !Array.isArray(value);