wrangler 0.0.0-e88a54e → 0.0.0-e95e5a0

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 (88) hide show
  1. package/README.md +25 -14
  2. package/bin/wrangler.js +13 -1
  3. package/miniflare-dist/index.mjs +4828 -21
  4. package/package.json +29 -24
  5. package/pages/functions/buildPlugin.ts +92 -0
  6. package/pages/functions/buildWorker.ts +80 -4
  7. package/pages/functions/filepath-routing.test.ts +11 -0
  8. package/pages/functions/filepath-routing.ts +8 -0
  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 +530 -201
  14. package/src/__tests__/dev.test.tsx +284 -18
  15. package/src/__tests__/guess-worker-format.test.ts +28 -1
  16. package/src/__tests__/helpers/mock-cfetch.ts +5 -1
  17. package/src/__tests__/helpers/mock-console.ts +20 -3
  18. package/src/__tests__/helpers/mock-dialogs.ts +26 -1
  19. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  20. package/src/__tests__/helpers/mock-istty.ts +27 -0
  21. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  22. package/src/__tests__/helpers/run-in-tmp.ts +7 -0
  23. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  24. package/src/__tests__/https-options.test.ts +5 -2
  25. package/src/__tests__/index.test.ts +27 -769
  26. package/src/__tests__/init.test.ts +1516 -0
  27. package/src/__tests__/jest.setup.ts +4 -4
  28. package/src/__tests__/kv.test.ts +133 -61
  29. package/src/__tests__/logger.test.ts +115 -0
  30. package/src/__tests__/pages.test.ts +311 -26
  31. package/src/__tests__/publish.test.ts +2789 -618
  32. package/src/__tests__/r2.test.ts +26 -10
  33. package/src/__tests__/route.test.ts +4 -4
  34. package/src/__tests__/secret.test.ts +55 -34
  35. package/src/__tests__/tail.test.ts +146 -20
  36. package/src/__tests__/user.test.ts +131 -0
  37. package/src/__tests__/whoami.test.tsx +14 -9
  38. package/src/bundle.ts +30 -3
  39. package/src/cfetch/internal.ts +0 -2
  40. package/src/cli.ts +5 -11
  41. package/src/config/README.md +1 -1
  42. package/src/config/config.ts +20 -4
  43. package/src/config/environment.ts +32 -4
  44. package/src/config/index.ts +4 -3
  45. package/src/config/validation-helpers.ts +96 -7
  46. package/src/config/validation.ts +388 -54
  47. package/src/config-cache.ts +45 -0
  48. package/src/create-worker-preview.ts +22 -13
  49. package/src/create-worker-upload-form.ts +32 -9
  50. package/src/dev/dev-vars.ts +36 -0
  51. package/src/dev/dev.tsx +132 -78
  52. package/src/dev/local.tsx +101 -21
  53. package/src/dev/remote.tsx +23 -13
  54. package/src/dev/use-esbuild.ts +23 -5
  55. package/src/dialogs.tsx +16 -6
  56. package/src/entry.ts +83 -24
  57. package/src/environment-variables.ts +3 -1
  58. package/src/errors.ts +11 -0
  59. package/src/https-options.ts +3 -2
  60. package/src/index.tsx +689 -394
  61. package/src/inspect.ts +17 -11
  62. package/src/intl-polyfill.d.ts +139 -0
  63. package/src/logger.ts +66 -0
  64. package/src/miniflare-cli/index.ts +4 -0
  65. package/src/miniflare-cli/request-context.ts +40 -0
  66. package/src/module-collection.ts +7 -7
  67. package/src/open-in-browser.ts +5 -4
  68. package/src/package-manager.ts +9 -8
  69. package/src/pages.tsx +875 -56
  70. package/src/parse.ts +1 -1
  71. package/src/proxy.ts +28 -19
  72. package/src/publish.ts +263 -141
  73. package/src/sites.tsx +27 -14
  74. package/src/tail/index.ts +2 -2
  75. package/src/tail/printing.ts +13 -8
  76. package/src/update-check.ts +19 -0
  77. package/src/user.tsx +116 -41
  78. package/src/whoami.tsx +2 -1
  79. package/src/worker.ts +15 -6
  80. package/templates/gitignore +171 -0
  81. package/templates/static-asset-facade.js +1 -1
  82. package/wrangler-dist/cli.js +21162 -50227
  83. package/miniflare-dist/index.mjs.map +0 -7
  84. package/src/__tests__/dev2.test.tsx +0 -154
  85. package/src/__tests__/logout.test.ts +0 -52
  86. package/src/__tests__/sentry.test.ts +0 -146
  87. package/src/reporting.ts +0 -131
  88. package/wrangler-dist/cli.js.map +0 -7
@@ -1,15 +1,21 @@
1
1
  import { render } from "ink-testing-library";
2
2
  import React from "react";
3
- import { reinitialiseAuthTokens, writeAuthConfigFile } from "../user";
3
+ import { writeAuthConfigFile } from "../user";
4
4
  import { getUserInfo, WhoAmI } from "../whoami";
5
5
  import { setMockResponse } from "./helpers/mock-cfetch";
6
6
  import { mockConsoleMethods } from "./helpers/mock-console";
7
+ import { useMockIsTTY } from "./helpers/mock-istty";
7
8
  import { runInTempDir } from "./helpers/run-in-tmp";
8
9
  import type { UserInfo } from "../whoami";
9
10
 
10
11
  describe("getUserInfo()", () => {
11
12
  runInTempDir({ homedir: "./home" });
12
13
  const std = mockConsoleMethods();
14
+ const { setIsTTY } = useMockIsTTY();
15
+
16
+ beforeEach(() => {
17
+ setIsTTY(true);
18
+ });
13
19
 
14
20
  it("should return undefined if there is no config file", async () => {
15
21
  const userInfo = await getUserInfo();
@@ -18,16 +24,12 @@ describe("getUserInfo()", () => {
18
24
 
19
25
  it("should return undefined if there is an empty config file", async () => {
20
26
  writeAuthConfigFile({});
21
- // Now that we have changed the auth tokens in the wrangler.toml we must reinitialize the user auth state.
22
- reinitialiseAuthTokens();
23
27
  const userInfo = await getUserInfo();
24
28
  expect(userInfo).toBeUndefined();
25
29
  });
26
30
 
27
31
  it("should return the user's email and accounts if authenticated via config token", async () => {
28
32
  writeAuthConfigFile({ oauth_token: "some-oauth-token" });
29
- // Now that we have changed the auth tokens in the wrangler.toml we must reinitialize the user auth state.
30
- reinitialiseAuthTokens();
31
33
 
32
34
  setMockResponse("/user", () => {
33
35
  return { email: "user@example.com" };
@@ -56,12 +58,15 @@ describe("getUserInfo()", () => {
56
58
 
57
59
  it("should display a warning message if the config file contains a legacy api_token field", async () => {
58
60
  writeAuthConfigFile({ api_token: "API_TOKEN" });
59
- await reinitialiseAuthTokens();
60
61
  await getUserInfo();
61
62
  expect(std.warn).toMatchInlineSnapshot(`
62
- "It looks like you have used Wrangler 1's \`config\` command to login with an API token.
63
- This is no longer supported in the current version of Wrangler.
64
- If you wish to authenticate via an API token then please set the \`CLOUDFLARE_API_TOKEN\` environment variable."
63
+ "▲ [WARNING] It looks like you have used Wrangler 1's \`config\` command to login with an API token.
64
+
65
+ This is no longer supported in the current version of Wrangler.
66
+ If you wish to authenticate via an API token then please set the \`CLOUDFLARE_API_TOKEN\`
67
+ environment variable.
68
+
69
+ "
65
70
  `);
66
71
  });
67
72
  });
package/src/bundle.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import assert from "node:assert";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill";
5
+ import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill";
4
6
  import * as esbuild from "esbuild";
5
7
  import createModuleCollector from "./module-collection";
6
8
  import type { Config } from "./config";
@@ -26,10 +28,21 @@ export async function bundleWorker(
26
28
  jsxFragment: string | undefined;
27
29
  rules: Config["rules"];
28
30
  watch?: esbuild.WatchMode;
31
+ tsconfig: string | undefined;
32
+ minify: boolean | undefined;
33
+ nodeCompat: boolean | undefined;
29
34
  }
30
35
  ): Promise<BundleResult> {
31
- const { serveAssetsFromWorker, jsxFactory, jsxFragment, rules, watch } =
32
- options;
36
+ const {
37
+ serveAssetsFromWorker,
38
+ jsxFactory,
39
+ jsxFragment,
40
+ rules,
41
+ watch,
42
+ tsconfig,
43
+ minify,
44
+ nodeCompat,
45
+ } = options;
33
46
  const entryDirectory = path.dirname(entry.file);
34
47
  const moduleCollector = createModuleCollector({
35
48
  wrangler1xlegacyModuleReferences: {
@@ -54,17 +67,31 @@ export async function bundleWorker(
54
67
  outdir: destination,
55
68
  external: ["__STATIC_CONTENT_MANIFEST"],
56
69
  format: "esm",
70
+ target: "es2020",
57
71
  sourcemap: true,
72
+ minify,
58
73
  metafile: true,
59
74
  conditions: ["worker", "browser"],
75
+ ...(process.env.NODE_ENV && {
76
+ define: {
77
+ "process.env.NODE_ENV": `"${process.env.NODE_ENV}"`,
78
+ ...(nodeCompat ? { global: "globalThis" } : {}),
79
+ },
80
+ }),
60
81
  loader: {
61
82
  ".js": "jsx",
62
83
  ".mjs": "jsx",
63
84
  ".cjs": "jsx",
64
85
  },
65
- plugins: [moduleCollector.plugin],
86
+ plugins: [
87
+ moduleCollector.plugin,
88
+ ...(nodeCompat
89
+ ? [NodeGlobalsPolyfills({ buffer: true }), NodeModulesPolyfills()]
90
+ : []),
91
+ ],
66
92
  ...(jsxFactory && { jsxFactory }),
67
93
  ...(jsxFragment && { jsxFragment }),
94
+ ...(tsconfig && { tsconfig }),
68
95
  watch,
69
96
  });
70
97
 
@@ -1,7 +1,6 @@
1
1
  import { fetch, Headers } from "undici";
2
2
  import { getEnvironmentVariableFactory } from "../environment-variables";
3
3
  import { ParseError, parseJSON } from "../parse";
4
- import { reportError } from "../reporting";
5
4
  import { getAPIToken, loginOrRefreshIfRequired } from "../user";
6
5
  import type { URLSearchParams } from "node:url";
7
6
  import type { RequestInit, HeadersInit } from "undici";
@@ -48,7 +47,6 @@ export async function fetchInternal<ResponseType>(
48
47
  try {
49
48
  return parseJSON(jsonText) as ResponseType;
50
49
  } catch (err) {
51
- await reportError(err as Error);
52
50
  throw new ParseError({
53
51
  text: "Received a malformed response from the API",
54
52
  notes: [
package/src/cli.ts CHANGED
@@ -1,19 +1,13 @@
1
+ import "dotenv/config"; // Grab locally specified env params from a `.env` file.
1
2
  import process from "process";
2
3
  import { hideBin } from "yargs/helpers";
3
- import { reportError, initReporting } from "./reporting";
4
+ import { FatalError } from "./errors";
4
5
  import { main } from ".";
5
6
 
6
- try {
7
- initReporting();
8
- } catch (error) {}
9
-
10
- process.on("uncaughtExceptionMonitor", async (err, origin) => {
11
- await reportError(err, origin);
12
- });
13
-
14
- main(hideBin(process.argv)).catch(() => {
7
+ main(hideBin(process.argv)).catch((e) => {
15
8
  // The logging of any error that was thrown from `main()` is handled in the `yargs.fail()` handler.
16
9
  // Here we just want to ensure that the process exits with a non-zero code.
17
10
  // We don't want to do this inside the `main()` function, since that would kill the process when running our tests.
18
- process.exit(1);
11
+ const exitCode = (e instanceof FatalError && e.code) || 1;
12
+ process.exit(exitCode);
19
13
  });
@@ -48,7 +48,7 @@ This function will return:
48
48
  - a `Diagnostics` object, which contains any errors or warnings from the validation process
49
49
 
50
50
  The field values may have been parsed directly from the `RawConfig`, inherited into a named environment from the top-level environment, or given a default value.
51
- Generally, if there are any warnings they should be presented to the user via `console.warn()` messages,
51
+ Generally, if there are any warnings they should be presented to the user via `logger.warn()` messages,
52
52
  and if there are any errors then an `Error` should be thrown describing these errors.
53
53
 
54
54
  The `Diagnostics` object is hierarchical: each `Diagnostics` instance can contain a collection of child `Diagnostics` instance.
@@ -135,6 +135,17 @@ export interface ConfigFields<Dev extends RawDevConfig> {
135
135
  [key: string]: string;
136
136
  }
137
137
  | undefined;
138
+
139
+ /**
140
+ * A list of data files that your worker should be bound to. This is
141
+ * the "legacy" way of binding to a data file. ES module workers should
142
+ * do proper module imports.
143
+ */
144
+ data_blobs:
145
+ | {
146
+ [key: string]: string;
147
+ }
148
+ | undefined;
138
149
  }
139
150
 
140
151
  export interface DevConfig {
@@ -142,7 +153,6 @@ export interface DevConfig {
142
153
  * IP address for the local dev server to listen on,
143
154
  *
144
155
  * @default `localhost`
145
- * @todo this needs to be implemented
146
156
  */
147
157
  ip: string;
148
158
 
@@ -157,7 +167,6 @@ export interface DevConfig {
157
167
  * Protocol that local wrangler dev server listens to requests on.
158
168
  *
159
169
  * @default `http`
160
- * @todo this needs to be implemented
161
170
  */
162
171
  local_protocol: "http" | "https";
163
172
 
@@ -168,13 +177,14 @@ export interface DevConfig {
168
177
  * See https://github.com/cloudflare/wrangler2/issues/583
169
178
  *
170
179
  * @default `https`
180
+ * @todo this needs to be implemented https://github.com/cloudflare/wrangler2/issues/583
171
181
  */
172
182
  upstream_protocol: "https" | "http";
173
183
 
174
184
  /**
175
- * Host to forward requests to, defaults to the zone of project
185
+ * Host to forward requests to, defaults to the host of the first route of project
176
186
  */
177
- host: string | undefined;
187
+ host: Config["route"];
178
188
  }
179
189
 
180
190
  export type RawDevConfig = Partial<DevConfig>;
@@ -197,6 +207,12 @@ export interface DeprecatedConfigFields {
197
207
  * @breaking
198
208
  */
199
209
  webpack_config?: string;
210
+
211
+ /**
212
+ * Configuration only used by a standalone use of the miniflare binary.
213
+ * @deprecated
214
+ */
215
+ miniflare?: unknown;
200
216
  }
201
217
 
202
218
  interface EnvironmentMap {
@@ -64,7 +64,7 @@ interface EnvironmentInheritable {
64
64
  * @breaking
65
65
  * @inheritable
66
66
  */
67
- workers_dev: boolean;
67
+ workers_dev: boolean | undefined;
68
68
 
69
69
  /**
70
70
  * A list of routes that your worker should be published to.
@@ -74,7 +74,13 @@ interface EnvironmentInheritable {
74
74
  *
75
75
  * @inheritable
76
76
  */
77
- routes: string[] | undefined;
77
+ routes:
78
+ | (
79
+ | string
80
+ | { pattern: string; zone_id: string }
81
+ | { pattern: string; zone_name: string }
82
+ )[]
83
+ | undefined;
78
84
 
79
85
  /**
80
86
  * A route that your worker should be published to. Literally
@@ -85,7 +91,18 @@ interface EnvironmentInheritable {
85
91
  *
86
92
  * @inheritable
87
93
  */
88
- 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;
89
106
 
90
107
  /**
91
108
  * The function to use to replace jsx syntax.
@@ -126,7 +143,7 @@ interface EnvironmentInheritable {
126
143
  *
127
144
  * @inheritable
128
145
  */
129
- usage_model: undefined | "bundled" | "unbound";
146
+ usage_model: "bundled" | "unbound" | undefined;
130
147
 
131
148
  /**
132
149
  * An ordered list of rules that define which modules to import,
@@ -160,6 +177,17 @@ interface EnvironmentInheritable {
160
177
  upload?: DeprecatedUpload;
161
178
  };
162
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
+
163
191
  /**
164
192
  * TODO: remove this as it has been deprecated.
165
193
  *
@@ -1,4 +1,5 @@
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";
@@ -21,7 +22,7 @@ export type {
21
22
  */
22
23
  export function readConfig(
23
24
  configPath: string | undefined,
24
- envName: string | undefined
25
+ args: unknown
25
26
  ): Config {
26
27
  let rawConfig: RawConfig = {};
27
28
  if (!configPath) {
@@ -37,11 +38,11 @@ export function readConfig(
37
38
  const { config, diagnostics } = normalizeAndValidateConfig(
38
39
  rawConfig,
39
40
  configPath,
40
- envName
41
+ args
41
42
  );
42
43
 
43
44
  if (diagnostics.hasWarnings()) {
44
- console.warn(diagnostics.renderWarnings());
45
+ logger.warn(diagnostics.renderWarnings());
45
46
  }
46
47
  if (diagnostics.hasErrors()) {
47
48
  throw new Error(diagnostics.renderErrors());
@@ -14,13 +14,15 @@ export function deprecated<T extends object>(
14
14
  fieldPath: DeepKeyOf<T>,
15
15
  message: string,
16
16
  remove: boolean,
17
- breaking = false
17
+ title = "Deprecation",
18
+ type: "warning" | "error" = "warning"
18
19
  ): void {
20
+ const BOLD = "\x1b[1m";
21
+ const NORMAL = "\x1b[0m";
22
+ const diagnosticMessage = `${BOLD}${title}${NORMAL}: "${fieldPath}":\n${message}`;
19
23
  const result = unwindPropertyPath(config, fieldPath);
20
24
  if (result !== undefined && result.field in result.container) {
21
- (breaking ? diagnostics.errors : diagnostics.warnings).push(
22
- `DEPRECATION: "${fieldPath}":\n${message}`
23
- );
25
+ diagnostics[`${type}s`].push(diagnosticMessage);
24
26
  if (remove) {
25
27
  delete (result.container as Record<string, unknown>)[result.field];
26
28
  }
@@ -58,11 +60,14 @@ export function inheritable<K extends keyof Environment>(
58
60
  rawEnv: RawEnvironment,
59
61
  field: K,
60
62
  validate: ValidatorFn,
61
- defaultValue: Environment[K]
63
+ defaultValue: Environment[K],
64
+ transformFn: TransformFn<Environment[K]> = (v) => v
62
65
  ): Environment[K] {
63
66
  validate(diagnostics, field, rawEnv[field], topLevelEnv);
64
67
  return (
65
- (rawEnv[field] as Environment[K]) ?? topLevelEnv?.[field] ?? defaultValue
68
+ (rawEnv[field] as Environment[K]) ??
69
+ transformFn(topLevelEnv?.[field]) ??
70
+ defaultValue
66
71
  );
67
72
  }
68
73
 
@@ -76,6 +81,7 @@ export function inheritableInLegacyEnvironments<K extends keyof Environment>(
76
81
  rawEnv: RawEnvironment,
77
82
  field: K,
78
83
  validate: ValidatorFn,
84
+ transformFn: TransformFn<Environment[K]> = (v) => v,
79
85
  defaultValue: Environment[K]
80
86
  ): Environment[K] {
81
87
  return topLevelEnv === undefined || isLegacyEnv === true
@@ -85,7 +91,8 @@ export function inheritableInLegacyEnvironments<K extends keyof Environment>(
85
91
  rawEnv,
86
92
  field,
87
93
  validate,
88
- defaultValue
94
+ defaultValue,
95
+ transformFn
89
96
  )
90
97
  : notAllowedInNamedServiceEnvironment(
91
98
  diagnostics,
@@ -95,6 +102,19 @@ export function inheritableInLegacyEnvironments<K extends keyof Environment>(
95
102
  );
96
103
  }
97
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
+
98
118
  /**
99
119
  * Log an error if this named environment is trying to override the value in the top-level
100
120
  * environment, which is not allow for this field.
@@ -207,6 +227,25 @@ export const isString: ValidatorFn = (diagnostics, field, value) => {
207
227
  return true;
208
228
  };
209
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
+
210
249
  /**
211
250
  * Validate that the field is an array of strings.
212
251
  */
@@ -484,3 +523,53 @@ export const validateAdditionalProperties = (
484
523
  }
485
524
  return true;
486
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);