wrangler 0.0.0-e95e5a0 → 0.0.0-e978986

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 (52) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +3 -1
  3. package/bin/wrangler.js +36 -4
  4. package/package.json +11 -7
  5. package/pages/functions/buildPlugin.ts +13 -0
  6. package/pages/functions/buildWorker.ts +14 -1
  7. package/pages/functions/template-plugin.ts +3 -2
  8. package/src/__tests__/configuration.test.ts +336 -87
  9. package/src/__tests__/dev.test.tsx +243 -25
  10. package/src/__tests__/helpers/mock-dialogs.ts +41 -1
  11. package/src/__tests__/index.test.ts +79 -72
  12. package/src/__tests__/init.test.ts +182 -131
  13. package/src/__tests__/jest.setup.ts +8 -0
  14. package/src/__tests__/kv.test.ts +173 -146
  15. package/src/__tests__/package-manager.test.ts +154 -7
  16. package/src/__tests__/pages.test.ts +381 -53
  17. package/src/__tests__/parse.test.ts +8 -0
  18. package/src/__tests__/publish.test.ts +682 -89
  19. package/src/__tests__/r2.test.ts +55 -36
  20. package/src/__tests__/secret.test.ts +39 -4
  21. package/src/abort.d.ts +3 -0
  22. package/src/bundle.ts +33 -2
  23. package/src/cfetch/index.ts +21 -4
  24. package/src/cfetch/internal.ts +17 -9
  25. package/src/config/config.ts +1 -1
  26. package/src/config/environment.ts +40 -14
  27. package/src/config/index.ts +162 -0
  28. package/src/config/validation.ts +180 -65
  29. package/src/create-worker-preview.ts +17 -7
  30. package/src/create-worker-upload-form.ts +22 -8
  31. package/src/dev/dev.tsx +7 -6
  32. package/src/dev/local.tsx +6 -0
  33. package/src/dev/remote.tsx +15 -1
  34. package/src/dialogs.tsx +48 -0
  35. package/src/durable.ts +102 -0
  36. package/src/entry.ts +64 -9
  37. package/src/index.tsx +357 -177
  38. package/src/inspect.ts +39 -0
  39. package/src/kv.ts +114 -25
  40. package/src/open-in-browser.ts +5 -12
  41. package/src/package-manager.ts +50 -3
  42. package/src/pages.tsx +271 -110
  43. package/src/parse.ts +23 -4
  44. package/src/proxy.ts +38 -22
  45. package/src/publish.ts +227 -113
  46. package/src/sites.tsx +19 -17
  47. package/src/user.tsx +11 -1
  48. package/src/worker.ts +8 -0
  49. package/templates/new-worker-scheduled.js +17 -0
  50. package/templates/new-worker-scheduled.ts +32 -0
  51. package/templates/new-worker.ts +16 -1
  52. package/wrangler-dist/cli.js +36017 -22687
@@ -17,6 +17,33 @@ describe("wrangler", () => {
17
17
 
18
18
  describe("r2", () => {
19
19
  describe("bucket", () => {
20
+ it("should show the correct help when an invalid command is passed", async () => {
21
+ await expect(() =>
22
+ runWrangler("r2 bucket foo")
23
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unknown argument: foo"`);
24
+ expect(std.err).toMatchInlineSnapshot(`
25
+ "X [ERROR] Unknown argument: foo
26
+
27
+ "
28
+ `);
29
+ expect(std.out).toMatchInlineSnapshot(`
30
+ "
31
+ wrangler r2 bucket
32
+
33
+ Manage R2 buckets
34
+
35
+ Commands:
36
+ wrangler r2 bucket create <name> Create a new R2 bucket
37
+ wrangler r2 bucket list List R2 buckets
38
+ wrangler r2 bucket delete <name> Delete an R2 bucket
39
+
40
+ Flags:
41
+ -c, --config Path to .toml configuration file [string]
42
+ -h, --help Show help [boolean]
43
+ -v, --version Show version number [boolean]"
44
+ `);
45
+ });
46
+
20
47
  describe("list", () => {
21
48
  function mockListRequest(buckets: R2BucketInfo[]) {
22
49
  const requests = { count: 0 };
@@ -69,10 +96,7 @@ describe("wrangler", () => {
69
96
  );
70
97
  expect(std.out).toMatchInlineSnapshot(`
71
98
  "
72
- "
73
- `);
74
- expect(std.err).toMatchInlineSnapshot(`
75
- "wrangler r2 bucket create <name>
99
+ wrangler r2 bucket create <name>
76
100
 
77
101
  Create a new R2 bucket
78
102
 
@@ -80,11 +104,12 @@ describe("wrangler", () => {
80
104
  name The name of the new bucket [string] [required]
81
105
 
82
106
  Flags:
83
- -c, --config Path to .toml configuration file [string]
84
- -h, --help Show help [boolean]
85
- -v, --version Show version number [boolean]
86
- --legacy-env Use legacy environments [boolean]
87
- X [ERROR] Not enough non-option arguments: got 0, need at least 1
107
+ -c, --config Path to .toml configuration file [string]
108
+ -h, --help Show help [boolean]
109
+ -v, --version Show version number [boolean]"
110
+ `);
111
+ expect(std.err).toMatchInlineSnapshot(`
112
+ "X [ERROR] Not enough non-option arguments: got 0, need at least 1
88
113
 
89
114
  "
90
115
  `);
@@ -98,10 +123,7 @@ describe("wrangler", () => {
98
123
  );
99
124
  expect(std.out).toMatchInlineSnapshot(`
100
125
  "
101
- "
102
- `);
103
- expect(std.err).toMatchInlineSnapshot(`
104
- "wrangler r2 bucket create <name>
126
+ wrangler r2 bucket create <name>
105
127
 
106
128
  Create a new R2 bucket
107
129
 
@@ -109,11 +131,12 @@ describe("wrangler", () => {
109
131
  name The name of the new bucket [string] [required]
110
132
 
111
133
  Flags:
112
- -c, --config Path to .toml configuration file [string]
113
- -h, --help Show help [boolean]
114
- -v, --version Show version number [boolean]
115
- --legacy-env Use legacy environments [boolean]
116
- X [ERROR] Unknown arguments: def, ghi
134
+ -c, --config Path to .toml configuration file [string]
135
+ -h, --help Show help [boolean]
136
+ -v, --version Show version number [boolean]"
137
+ `);
138
+ expect(std.err).toMatchInlineSnapshot(`
139
+ "X [ERROR] Unknown arguments: def, ghi
117
140
 
118
141
  "
119
142
  `);
@@ -153,10 +176,7 @@ describe("wrangler", () => {
153
176
  );
154
177
  expect(std.out).toMatchInlineSnapshot(`
155
178
  "
156
- "
157
- `);
158
- expect(std.err).toMatchInlineSnapshot(`
159
- "wrangler r2 bucket delete <name>
179
+ wrangler r2 bucket delete <name>
160
180
 
161
181
  Delete an R2 bucket
162
182
 
@@ -164,11 +184,12 @@ describe("wrangler", () => {
164
184
  name The name of the bucket to delete [string] [required]
165
185
 
166
186
  Flags:
167
- -c, --config Path to .toml configuration file [string]
168
- -h, --help Show help [boolean]
169
- -v, --version Show version number [boolean]
170
- --legacy-env Use legacy environments [boolean]
171
- X [ERROR] Not enough non-option arguments: got 0, need at least 1
187
+ -c, --config Path to .toml configuration file [string]
188
+ -h, --help Show help [boolean]
189
+ -v, --version Show version number [boolean]"
190
+ `);
191
+ expect(std.err).toMatchInlineSnapshot(`
192
+ "X [ERROR] Not enough non-option arguments: got 0, need at least 1
172
193
 
173
194
  "
174
195
  `);
@@ -182,10 +203,7 @@ describe("wrangler", () => {
182
203
  );
183
204
  expect(std.out).toMatchInlineSnapshot(`
184
205
  "
185
- "
186
- `);
187
- expect(std.err).toMatchInlineSnapshot(`
188
- "wrangler r2 bucket delete <name>
206
+ wrangler r2 bucket delete <name>
189
207
 
190
208
  Delete an R2 bucket
191
209
 
@@ -193,11 +211,12 @@ describe("wrangler", () => {
193
211
  name The name of the bucket to delete [string] [required]
194
212
 
195
213
  Flags:
196
- -c, --config Path to .toml configuration file [string]
197
- -h, --help Show help [boolean]
198
- -v, --version Show version number [boolean]
199
- --legacy-env Use legacy environments [boolean]
200
- X [ERROR] Unknown arguments: def, ghi
214
+ -c, --config Path to .toml configuration file [string]
215
+ -h, --help Show help [boolean]
216
+ -v, --version Show version number [boolean]"
217
+ `);
218
+ expect(std.err).toMatchInlineSnapshot(`
219
+ "X [ERROR] Unknown arguments: def, ghi
201
220
 
202
221
  "
203
222
  `);
@@ -60,6 +60,22 @@ describe("wrangler secret", () => {
60
60
  describe("interactive", () => {
61
61
  useMockStdin({ isTTY: true });
62
62
 
63
+ it("should trim stdin secret value", async () => {
64
+ mockPrompt({
65
+ text: "Enter a secret value:",
66
+ type: "password",
67
+ result: `hunter2
68
+ `,
69
+ });
70
+
71
+ mockPutRequest({ name: `secret-name`, text: `hunter2` });
72
+ await runWrangler("secret put secret-name --name script-name");
73
+ expect(std.out).toMatchInlineSnapshot(`
74
+ "🌀 Creating the secret for script script-name
75
+ ✨ Success! Uploaded secret secret-name"
76
+ `);
77
+ });
78
+
63
79
  it("should create a secret", async () => {
64
80
  mockPrompt({
65
81
  text: "Enter a secret value:",
@@ -132,7 +148,7 @@ describe("wrangler secret", () => {
132
148
  }
133
149
  expect(std.out).toMatchInlineSnapshot(`
134
150
  "
135
- If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
151
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
136
152
  `);
137
153
  expect(std.err).toMatchInlineSnapshot(`
138
154
  "X [ERROR] Missing script name
@@ -146,6 +162,25 @@ describe("wrangler secret", () => {
146
162
  describe("non-interactive", () => {
147
163
  const mockStdIn = useMockStdin({ isTTY: false });
148
164
 
165
+ it("should trim stdin secret value, from piped input", async () => {
166
+ mockPutRequest({ name: "the-key", text: "the-secret" });
167
+ // Pipe the secret in as three chunks to test that we reconstitute it correctly.
168
+ mockStdIn.send(
169
+ `the`,
170
+ `-`,
171
+ `secret
172
+ ` // whitespace & newline being removed
173
+ );
174
+ await runWrangler("secret put the-key --name script-name");
175
+
176
+ expect(std.out).toMatchInlineSnapshot(`
177
+ "🌀 Creating the secret for script script-name
178
+ ✨ Success! Uploaded secret the-key"
179
+ `);
180
+ expect(std.warn).toMatchInlineSnapshot(`""`);
181
+ expect(std.err).toMatchInlineSnapshot(`""`);
182
+ });
183
+
149
184
  it("should create a secret, from piped input", async () => {
150
185
  mockPutRequest({ name: "the-key", text: "the-secret" });
151
186
  // Pipe the secret in as three chunks to test that we reconstitute it correctly.
@@ -169,7 +204,7 @@ describe("wrangler secret", () => {
169
204
 
170
205
  expect(std.out).toMatchInlineSnapshot(`
171
206
  "
172
- If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
207
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
173
208
  `);
174
209
  expect(std.warn).toMatchInlineSnapshot(`""`);
175
210
  });
@@ -337,7 +372,7 @@ describe("wrangler secret", () => {
337
372
  }
338
373
  expect(std.out).toMatchInlineSnapshot(`
339
374
  "
340
- If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
375
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
341
376
  `);
342
377
  expect(std.err).toMatchInlineSnapshot(`
343
378
  "X [ERROR] Missing script name
@@ -433,7 +468,7 @@ describe("wrangler secret", () => {
433
468
  }
434
469
  expect(std.out).toMatchInlineSnapshot(`
435
470
  "
436
- If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
471
+ If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new/choose"
437
472
  `);
438
473
  expect(std.err).toMatchInlineSnapshot(`
439
474
  "X [ERROR] Missing script name
package/src/abort.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare interface AbortSignal {
2
+ addEventListener(event: "abort", handler: () => void);
3
+ }
package/src/bundle.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import * as fs from "node:fs";
3
+ import { builtinModules } from "node:module";
3
4
  import * as path from "node:path";
4
5
  import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill";
5
6
  import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill";
@@ -16,6 +17,33 @@ type BundleResult = {
16
17
  stop: (() => void) | undefined;
17
18
  };
18
19
 
20
+ /**
21
+ * Searches for any uses of node's builtin modules, and throws an error if it
22
+ * finds anything. This plugin is only used when nodeCompat is not enabled.
23
+ * Supports both regular node builtins, and the new "node:<MODULE>" format.
24
+ */
25
+ const checkForNodeBuiltinsPlugin = {
26
+ name: "checkForNodeBuiltins",
27
+ setup(build: esbuild.PluginBuild) {
28
+ build.onResolve(
29
+ {
30
+ filter: new RegExp(
31
+ "^(" +
32
+ builtinModules.join("|") +
33
+ "|" +
34
+ builtinModules.map((module) => "node:" + module).join("|") +
35
+ ")$"
36
+ ),
37
+ },
38
+ () => {
39
+ throw new Error(
40
+ `Detected a Node builtin module import while Node compatibility is disabled.\nAdd node_compat = true to your wrangler.toml file to enable Node compatibility.`
41
+ );
42
+ }
43
+ );
44
+ },
45
+ };
46
+
19
47
  /**
20
48
  * Generate a bundle for the worker identified by the arguments passed in.
21
49
  */
@@ -60,13 +88,14 @@ export async function bundleWorker(
60
88
  format: entry.format,
61
89
  rules,
62
90
  });
91
+
63
92
  const result = await esbuild.build({
64
93
  ...getEntryPoint(entry.file, serveAssetsFromWorker),
65
94
  bundle: true,
66
95
  absWorkingDir: entry.directory,
67
96
  outdir: destination,
68
97
  external: ["__STATIC_CONTENT_MANIFEST"],
69
- format: "esm",
98
+ format: entry.format === "modules" ? "esm" : "iife",
70
99
  target: "es2020",
71
100
  sourcemap: true,
72
101
  minify,
@@ -87,7 +116,9 @@ export async function bundleWorker(
87
116
  moduleCollector.plugin,
88
117
  ...(nodeCompat
89
118
  ? [NodeGlobalsPolyfills({ buffer: true }), NodeModulesPolyfills()]
90
- : []),
119
+ : // we use checkForNodeBuiltinsPlugin to throw a nicer error
120
+ // if we find node builtins when nodeCompat isn't turned on
121
+ [checkForNodeBuiltinsPlugin]),
91
122
  ],
92
123
  ...(jsxFactory && { jsxFactory }),
93
124
  ...(jsxFragment && { jsxFragment }),
@@ -10,6 +10,7 @@ export { getCloudflareAPIBaseURL as getCloudflareApiBaseUrl } from "./internal";
10
10
  export interface FetchError {
11
11
  code: number;
12
12
  message: string;
13
+ error_chain?: FetchError[];
13
14
  }
14
15
  export interface FetchResult<ResponseType = unknown> {
15
16
  success: boolean;
@@ -27,12 +28,14 @@ export { fetchKVGetValue } from "./internal";
27
28
  export async function fetchResult<ResponseType>(
28
29
  resource: string,
29
30
  init: RequestInit = {},
30
- queryParams?: URLSearchParams
31
+ queryParams?: URLSearchParams,
32
+ abortSignal?: AbortSignal
31
33
  ): Promise<ResponseType> {
32
34
  const json = await fetchInternal<FetchResult<ResponseType>>(
33
35
  resource,
34
36
  init,
35
- queryParams
37
+ queryParams,
38
+ abortSignal
36
39
  );
37
40
  if (json.success) {
38
41
  return json.result;
@@ -83,9 +86,9 @@ function throwFetchError(
83
86
  response: FetchResult<unknown>
84
87
  ): never {
85
88
  const error = new ParseError({
86
- text: "Received a bad response from the API",
89
+ text: `A request to the Cloudflare API (${resource}) failed.`,
87
90
  notes: response.errors.map((err) => ({
88
- text: err.code ? `${err.message} [code: ${err.code}]` : err.message,
91
+ text: renderError(err),
89
92
  })),
90
93
  });
91
94
  // add the first error code directly to this error
@@ -102,3 +105,17 @@ function hasCursor(result_info: unknown): result_info is { cursor: string } {
102
105
  const cursor = (result_info as { cursor: string } | undefined)?.cursor;
103
106
  return cursor !== undefined && cursor !== null && cursor !== "";
104
107
  }
108
+
109
+ function renderError(err: FetchError, level = 0): string {
110
+ const chainedMessages =
111
+ err.error_chain
112
+ ?.map(
113
+ (chainedError) =>
114
+ `\n${" ".repeat(level)}- ${renderError(chainedError, level + 1)}`
115
+ )
116
+ .join("\n") ?? "";
117
+ return (
118
+ (err.code ? `${err.message} [code: ${err.code}]` : err.message) +
119
+ chainedMessages
120
+ );
121
+ }
@@ -1,4 +1,5 @@
1
1
  import { fetch, Headers } from "undici";
2
+ import { version as wranglerVersion } from "../../package.json";
2
3
  import { getEnvironmentVariableFactory } from "../environment-variables";
3
4
  import { ParseError, parseJSON } from "../parse";
4
5
  import { getAPIToken, loginOrRefreshIfRequired } from "../user";
@@ -26,12 +27,14 @@ export const getCloudflareAPIBaseURL = getEnvironmentVariableFactory({
26
27
  export async function fetchInternal<ResponseType>(
27
28
  resource: string,
28
29
  init: RequestInit = {},
29
- queryParams?: URLSearchParams
30
+ queryParams?: URLSearchParams,
31
+ abortSignal?: AbortSignal
30
32
  ): Promise<ResponseType> {
31
33
  await requireLoggedIn();
32
34
  const apiToken = requireApiToken();
33
35
  const headers = cloneHeaders(init.headers);
34
- addAuthorizationHeader(headers, apiToken);
36
+ addAuthorizationHeaderIfUnspecified(headers, apiToken);
37
+ addUserAgent(headers);
35
38
 
36
39
  const queryString = queryParams ? `?${queryParams.toString()}` : "";
37
40
  const method = init.method ?? "GET";
@@ -41,11 +44,12 @@ export async function fetchInternal<ResponseType>(
41
44
  method,
42
45
  ...init,
43
46
  headers,
47
+ signal: abortSignal,
44
48
  }
45
49
  );
46
50
  const jsonText = await response.text();
47
51
  try {
48
- return parseJSON(jsonText) as ResponseType;
52
+ return parseJSON<ResponseType>(jsonText);
49
53
  } catch (err) {
50
54
  throw new ParseError({
51
55
  text: "Received a malformed response from the API",
@@ -94,16 +98,17 @@ function requireApiToken(): string {
94
98
  return authToken;
95
99
  }
96
100
 
97
- function addAuthorizationHeader(
101
+ function addAuthorizationHeaderIfUnspecified(
98
102
  headers: Record<string, string>,
99
103
  apiToken: string
100
104
  ): void {
101
- if ("Authorization" in headers) {
102
- throw new Error(
103
- "The request already specifies an authorisation header - cannot add a new one."
104
- );
105
+ if (!("Authorization" in headers)) {
106
+ headers["Authorization"] = `Bearer ${apiToken}`;
105
107
  }
106
- headers["Authorization"] = `Bearer ${apiToken}`;
108
+ }
109
+
110
+ function addUserAgent(headers: Record<string, string>): void {
111
+ headers["User-Agent"] = `wrangler/${wranglerVersion}`;
107
112
  }
108
113
 
109
114
  /**
@@ -112,6 +117,9 @@ function addAuthorizationHeader(
112
117
  * doesn't return json. We inline the implementation and try not to share
113
118
  * any code with the other calls. We should push back on any new APIs that
114
119
  * try to introduce non-"standard" response structures.
120
+ *
121
+ * Note: any calls to fetchKVGetValue must call encodeURIComponent on key
122
+ * before passing it
115
123
  */
116
124
 
117
125
  export async function fetchKVGetValue(
@@ -161,7 +161,7 @@ export interface DevConfig {
161
161
  *
162
162
  * @default `8787`
163
163
  */
164
- port: number;
164
+ port: number | undefined;
165
165
 
166
166
  /**
167
167
  * Protocol that local wrangler dev server listens to requests on.
@@ -8,6 +8,24 @@ export interface Environment
8
8
  extends EnvironmentInheritable,
9
9
  EnvironmentNonInheritable {}
10
10
 
11
+ export type SimpleRoute = string;
12
+ export type ZoneIdRoute = {
13
+ pattern: string;
14
+ zone_id: string;
15
+ custom_domain?: boolean;
16
+ };
17
+ export type ZoneNameRoute = {
18
+ pattern: string;
19
+ zone_name: string;
20
+ custom_domain?: boolean;
21
+ };
22
+ export type CustomDomainRoute = { pattern: string; custom_domain: boolean };
23
+ export type Route =
24
+ | SimpleRoute
25
+ | ZoneIdRoute
26
+ | ZoneNameRoute
27
+ | CustomDomainRoute;
28
+
11
29
  /**
12
30
  * The `EnvironmentInheritable` interface declares all the configuration fields for an environment
13
31
  * that can be inherited (and overridden) from the top-level environment.
@@ -74,13 +92,7 @@ interface EnvironmentInheritable {
74
92
  *
75
93
  * @inheritable
76
94
  */
77
- routes:
78
- | (
79
- | string
80
- | { pattern: string; zone_id: string }
81
- | { pattern: string; zone_name: string }
82
- )[]
83
- | undefined;
95
+ routes: Route[] | undefined;
84
96
 
85
97
  /**
86
98
  * A route that your worker should be published to. Literally
@@ -91,13 +103,7 @@ interface EnvironmentInheritable {
91
103
  *
92
104
  * @inheritable
93
105
  */
94
- route:
95
- | (
96
- | string
97
- | { pattern: string; zone_id: string }
98
- | { pattern: string; zone_name: string }
99
- )
100
- | undefined;
106
+ route: Route | undefined;
101
107
 
102
108
  /**
103
109
  * Path to a custom tsconfig
@@ -236,6 +242,8 @@ interface EnvironmentNonInheritable {
236
242
  class_name: string;
237
243
  /** The script where the Durable Object is defined (if it's external to this worker) */
238
244
  script_name?: string;
245
+ /** The service environment of the script_name to bind to */
246
+ environment?: string;
239
247
  }[];
240
248
  };
241
249
 
@@ -279,6 +287,24 @@ interface EnvironmentNonInheritable {
279
287
  preview_bucket_name?: string;
280
288
  }[];
281
289
 
290
+ /**
291
+ * Specifies service bindings (worker-to-worker) that are bound to this Worker environment.
292
+ *
293
+ * NOTE: This field is not automatically inherited from the top level environment,
294
+ * and so must be specified in every named environment.
295
+ *
296
+ * @default `[]`
297
+ * @nonInheritable
298
+ */
299
+ services: {
300
+ /** The binding name used to refer to the bound service. */
301
+ binding: string;
302
+ /** The name of the service. */
303
+ service: string;
304
+ /** The environment of the service (e.g. production, staging, etc). */
305
+ environment?: string;
306
+ }[];
307
+
282
308
  /**
283
309
  * "Unsafe" tables for features that aren't directly supported by wrangler.
284
310
  *