wrangler 2.4.4 → 2.6.0

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 (46) hide show
  1. package/bin/wrangler.js +20 -8
  2. package/miniflare-dist/index.mjs +90 -41
  3. package/package.json +3 -3
  4. package/src/__tests__/configuration.test.ts +211 -0
  5. package/src/__tests__/delete.test.ts +81 -48
  6. package/src/__tests__/dev.test.tsx +25 -8
  7. package/src/__tests__/helpers/mock-oauth-flow.ts +5 -1
  8. package/src/__tests__/helpers/msw/handlers/oauth.ts +13 -18
  9. package/src/__tests__/init.test.ts +18 -3
  10. package/src/__tests__/logout.test.ts +47 -0
  11. package/src/__tests__/metrics.test.ts +88 -43
  12. package/src/__tests__/pages-deployment-tail.test.ts +165 -101
  13. package/src/__tests__/publish.test.ts +94 -7
  14. package/src/__tests__/pubsub.test.ts +208 -88
  15. package/src/__tests__/queues.test.ts +155 -67
  16. package/src/__tests__/tail.test.ts +207 -108
  17. package/src/__tests__/type-generation.test.ts +7 -0
  18. package/src/__tests__/user.test.ts +43 -69
  19. package/src/config/environment.ts +16 -0
  20. package/src/config/index.ts +31 -8
  21. package/src/config/validation.ts +49 -0
  22. package/src/create-worker-upload-form.ts +9 -0
  23. package/src/d1/backups.tsx +7 -2
  24. package/src/d1/delete.tsx +4 -4
  25. package/src/d1/index.ts +2 -0
  26. package/src/d1/migrations/apply.tsx +6 -5
  27. package/src/d1/migrations/helpers.ts +4 -2
  28. package/src/d1/migrations/list.tsx +2 -2
  29. package/src/d1/migrations/options.ts +18 -0
  30. package/src/dev/dev.tsx +46 -22
  31. package/src/dev/local.tsx +63 -29
  32. package/src/dev/start-server.ts +18 -21
  33. package/src/dev.tsx +33 -13
  34. package/src/git-client.ts +15 -4
  35. package/src/index.tsx +1 -0
  36. package/src/init.ts +8 -0
  37. package/src/miniflare-cli/assets.ts +55 -28
  38. package/src/miniflare-cli/index.ts +2 -1
  39. package/src/pages/dev.tsx +7 -0
  40. package/src/proxy.ts +5 -0
  41. package/src/publish/publish.ts +1 -0
  42. package/src/secret/index.ts +1 -0
  43. package/src/type-generation.ts +10 -2
  44. package/src/worker.ts +6 -0
  45. package/wrangler-dist/cli.d.ts +15 -0
  46. package/wrangler-dist/cli.js +2045 -636
@@ -1,18 +1,19 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import fetchMock from "jest-fetch-mock";
4
- import { getGlobalWranglerConfigPath } from "../global-wrangler-config-path";
1
+ import { rest } from "msw";
5
2
  import { CI } from "../is-ci";
6
3
  import {
7
4
  loginOrRefreshIfRequired,
8
5
  readAuthConfigFile,
9
6
  requireAuth,
10
- USER_AUTH_CONFIG_FILE,
11
7
  writeAuthConfigFile,
12
8
  } from "../user";
13
9
  import { mockConsoleMethods } from "./helpers/mock-console";
14
10
  import { useMockIsTTY } from "./helpers/mock-istty";
15
11
  import { mockOAuthFlow } from "./helpers/mock-oauth-flow";
12
+ import {
13
+ msw,
14
+ mswSuccessOauthHandlers,
15
+ mswSuccessUserHandlers,
16
+ } from "./helpers/msw";
16
17
  import { runInTempDir } from "./helpers/run-in-tmp";
17
18
  import { runWrangler } from "./helpers/run-wrangler";
18
19
  import type { Config } from "../config";
@@ -22,41 +23,44 @@ describe("User", () => {
22
23
  let isCISpy: jest.SpyInstance;
23
24
  runInTempDir();
24
25
  const std = mockConsoleMethods();
25
- const {
26
- mockOAuthServerCallback,
27
- mockGrantAccessToken,
28
- mockGrantAuthorization,
29
- mockRevokeAuthorization,
30
- mockExchangeRefreshTokenForAccessToken,
31
- } = mockOAuthFlow();
32
-
26
+ // TODO: Implement these two mocks with MSW
27
+ const { mockOAuthServerCallback } = mockOAuthFlow();
33
28
  const { setIsTTY } = useMockIsTTY();
34
29
 
35
30
  beforeEach(() => {
31
+ msw.use(...mswSuccessOauthHandlers, ...mswSuccessUserHandlers);
36
32
  isCISpy = jest.spyOn(CI, "isCI").mockReturnValue(false);
37
33
  });
38
34
 
39
35
  describe("login", () => {
40
36
  it("should login a user when `wrangler login` is run", async () => {
41
- mockOAuthServerCallback();
42
- const accessTokenRequest = mockGrantAccessToken({ respondWith: "ok" });
43
- mockGrantAuthorization({ respondWith: "success" });
37
+ mockOAuthServerCallback("success");
38
+
39
+ let counter = 0;
40
+ msw.use(
41
+ rest.post("*/oauth2/token", async (_, response, context) => {
42
+ counter += 1;
43
+
44
+ return response.once(
45
+ context.status(200),
46
+ context.json({
47
+ access_token: "test-access-token",
48
+ expires_in: 100000,
49
+ refresh_token: "test-refresh-token",
50
+ scope: "account:read",
51
+ })
52
+ );
53
+ })
54
+ );
44
55
 
45
56
  await runWrangler("login");
46
57
 
47
- expect(accessTokenRequest.actual.url).toEqual(
48
- accessTokenRequest.expected.url
49
- );
50
- expect(accessTokenRequest.actual.method).toEqual(
51
- accessTokenRequest.expected.method
52
- );
53
-
58
+ expect(counter).toBe(1);
54
59
  expect(std.out).toMatchInlineSnapshot(`
55
60
  "Attempting to login via OAuth...
56
61
  Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256
57
62
  Successfully logged in."
58
63
  `);
59
-
60
64
  expect(readAuthConfigFile()).toEqual<UserAuthConfig>({
61
65
  api_token: undefined,
62
66
  oauth_token: "test-access-token",
@@ -67,50 +71,25 @@ describe("User", () => {
67
71
  });
68
72
  });
69
73
 
70
- describe("logout", () => {
71
- it("should exit with a message stating the user is not logged in", async () => {
72
- await runWrangler("logout");
73
- expect(std.out).toMatchInlineSnapshot(`"Not logged in, exiting..."`);
74
- });
75
-
76
- it("should logout user that has been properly logged in", async () => {
77
- writeAuthConfigFile({
78
- oauth_token: "some-oauth-tok",
79
- refresh_token: "some-refresh-tok",
80
- });
81
- const outcome = mockRevokeAuthorization();
82
-
83
- await runWrangler("logout");
84
-
85
- expect(outcome.actual.url).toEqual(
86
- "https://dash.cloudflare.com/oauth2/revoke"
87
- );
88
- expect(outcome.actual.method).toEqual("POST");
89
-
90
- expect(std.out).toMatchInlineSnapshot(`"Successfully logged out."`);
91
-
92
- // Make sure that we made the request to logout.
93
- expect(fetchMock).toHaveBeenCalledTimes(1);
94
-
95
- // Make sure that logout removed the config file containing the auth tokens.
96
- const config = path.join(
97
- getGlobalWranglerConfigPath(),
98
- USER_AUTH_CONFIG_FILE
99
- );
100
- expect(fs.existsSync(config)).toBeFalsy();
101
- });
102
- });
103
-
104
- // TODO: Improve OAuth mocking to handle `/token` endpoints from different calls
105
- it("should handle errors for failed token refresh", async () => {
74
+ it("should handle errors for failed token refresh in a non-interactive environment", async () => {
106
75
  setIsTTY(false);
107
76
  writeAuthConfigFile({
108
77
  oauth_token: "hunter2",
109
78
  refresh_token: "Order 66",
110
79
  });
111
- mockExchangeRefreshTokenForAccessToken({
112
- respondWith: "badResponse",
113
- });
80
+ // TODO: Use MSW to handle `/token` endpoints from different calls
81
+ let counter = 0;
82
+ msw.use(
83
+ rest.post("*/oauth2/token", async (request, response, context) => {
84
+ counter += 1;
85
+ return response.once(
86
+ context.status(400),
87
+ context.body(
88
+ `<html> <body> This shouldn't be sent, but should be handled </body> </html>`
89
+ )
90
+ );
91
+ })
92
+ );
114
93
 
115
94
  // Handles the requireAuth error throw from failed login that is unhandled due to directly calling it here
116
95
  await expect(
@@ -118,20 +97,15 @@ describe("User", () => {
118
97
  ).rejects.toThrowErrorMatchingInlineSnapshot(
119
98
  `"In a non-interactive environment, it's necessary to set a CLOUDFLARE_API_TOKEN environment variable for wrangler to work. Please go to https://developers.cloudflare.com/api/tokens/create/ for instructions on how to create an api token, and assign its value to CLOUDFLARE_API_TOKEN."`
120
99
  );
100
+ expect(counter).toBe(0);
121
101
  });
122
102
 
123
103
  it("should confirm no error message when refresh is successful", async () => {
124
104
  setIsTTY(false);
125
- mockOAuthServerCallback();
126
105
  writeAuthConfigFile({
127
106
  oauth_token: "hunter2",
128
107
  refresh_token: "Order 66",
129
108
  });
130
- mockGrantAuthorization({ respondWith: "success" });
131
-
132
- mockExchangeRefreshTokenForAccessToken({
133
- respondWith: "refreshSuccess",
134
- });
135
109
 
136
110
  // Handles the requireAuth error throw from failed login that is unhandled due to directly calling it here
137
111
  await expect(requireAuth({} as Config)).rejects.toThrowError();
@@ -439,6 +439,22 @@ interface EnvironmentNonInheritable {
439
439
  }[]
440
440
  | undefined;
441
441
 
442
+ /**
443
+ * Specifies analytics engine datasets that are bound to this Worker environment.
444
+ *
445
+ * NOTE: This field is not automatically inherited from the top level environment,
446
+ * and so must be specified in every named environment.
447
+ *
448
+ * @default `[]`
449
+ * @nonInheritable
450
+ */
451
+ analytics_engine_datasets: {
452
+ /** The binding name used to refer to the dataset in the worker. */
453
+ binding: string;
454
+ /** The name of this dataset to write to. */
455
+ dataset?: string;
456
+ }[];
457
+
442
458
  /**
443
459
  * "Unsafe" tables for features that aren't directly supported by wrangler.
444
460
  *
@@ -93,6 +93,7 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) {
93
93
  r2_buckets,
94
94
  logfwdr,
95
95
  services,
96
+ analytics_engine_datasets,
96
97
  text_blobs,
97
98
  unsafe,
98
99
  vars,
@@ -159,14 +160,21 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) {
159
160
  if (d1_databases !== undefined && d1_databases.length > 0) {
160
161
  output.push({
161
162
  type: "D1 Databases",
162
- entries: d1_databases.map(({ binding, database_name, database_id }) => {
163
- return {
164
- key: removeD1BetaPrefix(binding),
165
- value: database_name
166
- ? `${database_name} (${database_id})`
167
- : database_id,
168
- };
169
- }),
163
+ entries: d1_databases.map(
164
+ ({ binding, database_name, database_id, preview_database_id }) => {
165
+ let databaseValue = `${database_id}`;
166
+ if (database_name) {
167
+ databaseValue = `${database_name} (${database_id})`;
168
+ }
169
+ if (preview_database_id) {
170
+ databaseValue += `, Preview: (${preview_database_id})`;
171
+ }
172
+ return {
173
+ key: removeD1BetaPrefix(binding),
174
+ value: databaseValue,
175
+ };
176
+ }
177
+ ),
170
178
  });
171
179
  }
172
180
 
@@ -211,6 +219,21 @@ export function printBindings(bindings: CfWorkerInit["bindings"]) {
211
219
  });
212
220
  }
213
221
 
222
+ if (
223
+ analytics_engine_datasets !== undefined &&
224
+ analytics_engine_datasets.length > 0
225
+ ) {
226
+ output.push({
227
+ type: "Analytics Engine Datasets",
228
+ entries: analytics_engine_datasets.map(({ binding, dataset }) => {
229
+ return {
230
+ key: binding,
231
+ value: dataset ?? binding,
232
+ };
233
+ }),
234
+ });
235
+ }
236
+
214
237
  if (text_blobs !== undefined && Object.keys(text_blobs).length > 0) {
215
238
  output.push({
216
239
  type: "Text Blobs",
@@ -1115,6 +1115,16 @@ function normalizeAndValidateEnvironment(
1115
1115
  validateBindingArray(envName, validateServiceBinding),
1116
1116
  []
1117
1117
  ),
1118
+ analytics_engine_datasets: notInheritable(
1119
+ diagnostics,
1120
+ topLevelEnv,
1121
+ rawConfig,
1122
+ rawEnv,
1123
+ envName,
1124
+ "analytics_engine_datasets",
1125
+ validateBindingArray(envName, validateAnalyticsEngineBinding),
1126
+ []
1127
+ ),
1118
1128
  dispatch_namespaces: notInheritable(
1119
1129
  diagnostics,
1120
1130
  topLevelEnv,
@@ -1834,6 +1844,7 @@ const validateBindingsHaveUniqueNames = (
1834
1844
  durable_objects,
1835
1845
  kv_namespaces,
1836
1846
  r2_buckets,
1847
+ analytics_engine_datasets,
1837
1848
  text_blobs,
1838
1849
  unsafe,
1839
1850
  vars,
@@ -1848,6 +1859,7 @@ const validateBindingsHaveUniqueNames = (
1848
1859
  "Durable Object": getBindingNames(durable_objects),
1849
1860
  "KV Namespace": getBindingNames(kv_namespaces),
1850
1861
  "R2 Bucket": getBindingNames(r2_buckets),
1862
+ "Analytics Engine Dataset": getBindingNames(analytics_engine_datasets),
1851
1863
  "Text Blob": getBindingNames(text_blobs),
1852
1864
  Unsafe: getBindingNames(unsafe),
1853
1865
  "Environment Variable": getBindingNames(vars),
@@ -1956,6 +1968,43 @@ const validateServiceBinding: ValidatorFn = (diagnostics, field, value) => {
1956
1968
  return isValid;
1957
1969
  };
1958
1970
 
1971
+ const validateAnalyticsEngineBinding: ValidatorFn = (
1972
+ diagnostics,
1973
+ field,
1974
+ value
1975
+ ) => {
1976
+ if (typeof value !== "object" || value === null) {
1977
+ diagnostics.errors.push(
1978
+ `"analytics_engine" bindings should be objects, but got ${JSON.stringify(
1979
+ value
1980
+ )}`
1981
+ );
1982
+ return false;
1983
+ }
1984
+ let isValid = true;
1985
+ // Service bindings must have a binding and optional dataset.
1986
+ if (!isRequiredProperty(value, "binding", "string")) {
1987
+ diagnostics.errors.push(
1988
+ `"${field}" bindings should have a string "binding" field but got ${JSON.stringify(
1989
+ value
1990
+ )}.`
1991
+ );
1992
+ isValid = false;
1993
+ }
1994
+ if (
1995
+ !isOptionalProperty(value, "dataset", "string") ||
1996
+ (value as { dataset: string }).dataset?.length === 0
1997
+ ) {
1998
+ diagnostics.errors.push(
1999
+ `"${field}" bindings should, optionally, have a string "dataset" field but got ${JSON.stringify(
2000
+ value
2001
+ )}.`
2002
+ );
2003
+ isValid = false;
2004
+ }
2005
+ return isValid;
2006
+ };
2007
+
1959
2008
  const validateWorkerNamespaceBinding: ValidatorFn = (
1960
2009
  diagnostics,
1961
2010
  field,
@@ -43,6 +43,7 @@ type WorkerMetadataBinding =
43
43
  | { type: "r2_bucket"; name: string; bucket_name: string }
44
44
  | { type: "d1"; name: string; id: string; internalEnv?: string }
45
45
  | { type: "service"; name: string; service: string; environment?: string }
46
+ | { type: "analytics_engine"; name: string; dataset?: string }
46
47
  | { type: "namespace"; name: string; namespace: string }
47
48
  | {
48
49
  type: "logfwdr";
@@ -149,6 +150,14 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
149
150
  });
150
151
  });
151
152
 
153
+ bindings.analytics_engine_datasets?.forEach(({ binding, dataset }) => {
154
+ metadataBindings.push({
155
+ name: binding,
156
+ type: "analytics_engine",
157
+ dataset,
158
+ });
159
+ });
160
+
152
161
  bindings.dispatch_namespaces?.forEach(({ binding, namespace }) => {
153
162
  metadataBindings.push({
154
163
  name: binding,
@@ -141,9 +141,9 @@ export const RestoreHandler = withConfig<BackupRestoreArgs>(
141
141
  name
142
142
  );
143
143
 
144
- console.log(`Restoring ${name} from backup ${backupId}....`);
144
+ logger.log(`Restoring ${name} from backup ${backupId}....`);
145
145
  await restoreBackup(accountId, db.uuid, backupId);
146
- console.log(`Done!`);
146
+ logger.log(`Done!`);
147
147
  }
148
148
  );
149
149
 
@@ -199,6 +199,11 @@ export const DownloadHandler = withConfig<BackupDownloadArgs>(
199
199
 
200
200
  logger.log(`🌀 Downloading backup ${backupId} from '${name}'`);
201
201
  const response = await getBackupResponse(accountId, db.uuid, backupId);
202
+ if (!response.ok) {
203
+ throw new Error(
204
+ `Failed to download backup ${backupId} from '${name}' - got ${response.status} from the API`
205
+ );
206
+ }
202
207
  logger.log(`🌀 Saving to ${filename}`);
203
208
  // TODO: stream this once we upgrade to Node18 and can use Writable.fromWeb
204
209
  const buffer = await response.arrayBuffer();
package/src/d1/delete.tsx CHANGED
@@ -36,21 +36,21 @@ export const Handler = withConfig<CreateArgs>(
36
36
  name
37
37
  );
38
38
 
39
- console.log(`About to delete DB '${name}' (${db.uuid}).`);
39
+ logger.log(`About to delete DB '${name}' (${db.uuid}).`);
40
40
  if (!skipConfirmation) {
41
41
  const response = await confirm(`Ok to proceed?`);
42
42
  if (!response) {
43
- console.log(`Not deleting.`);
43
+ logger.log(`Not deleting.`);
44
44
  return;
45
45
  }
46
46
  }
47
47
 
48
- console.log("Deleting...");
48
+ logger.log("Deleting...");
49
49
 
50
50
  await fetchResult(`/accounts/${accountId}/d1/database/${db.uuid}`, {
51
51
  method: "DELETE",
52
52
  });
53
53
 
54
- console.log(`Deleted '${name}' successfully.`);
54
+ logger.log(`Deleted '${name}' successfully.`);
55
55
  }
56
56
  );
package/src/d1/index.ts CHANGED
@@ -26,6 +26,7 @@ export const d1 = (yargs: Argv<CommonYargsOptions>) => {
26
26
  )
27
27
  .command("backup", "Interact with D1 Backups", (yargs2) =>
28
28
  yargs2
29
+ .demandCommand()
29
30
  .command(
30
31
  "list <name>",
31
32
  "List your D1 backups",
@@ -74,6 +75,7 @@ export const d1 = (yargs: Argv<CommonYargsOptions>) => {
74
75
  )
75
76
  .command("migrations", "Interact with D1 Migrations", (yargs2) =>
76
77
  yargs2
78
+ .demandCommand()
77
79
  .command(
78
80
  "list <database>",
79
81
  "List your D1 migrations",
@@ -5,23 +5,25 @@ import Table from "ink-table";
5
5
  import React from "react";
6
6
  import { withConfig } from "../../config";
7
7
  import { confirm } from "../../dialogs";
8
+ import { CI } from "../../is-ci";
9
+ import isInteractive from "../../is-interactive";
8
10
  import { logger } from "../../logger";
9
11
  import { requireAuth } from "../../user";
10
12
  import { createBackup } from "../backups";
11
13
  import { executeSql } from "../execute";
12
- import { Database } from "../options";
13
14
  import { d1BetaWarning, getDatabaseInfoFromConfig } from "../utils";
14
15
  import {
15
16
  getMigrationsPath,
16
17
  getUnappliedMigrations,
17
18
  initMigrationsTable,
18
19
  } from "./helpers";
20
+ import { DatabaseWithLocal } from "./options";
19
21
  import type { ParseError } from "../../parse";
20
22
  import type { BaseSqlExecuteArgs } from "../execute";
21
23
  import type { Argv } from "yargs";
22
24
 
23
25
  export function ApplyOptions(yargs: Argv): Argv<BaseSqlExecuteArgs> {
24
- return Database(yargs);
26
+ return DatabaseWithLocal(yargs);
25
27
  }
26
28
 
27
29
  export const ApplyHandler = withConfig<BaseSqlExecuteArgs>(
@@ -88,8 +90,7 @@ export const ApplyHandler = withConfig<BaseSqlExecuteArgs>(
88
90
  return;
89
91
  }
90
92
 
91
- const isInteractive = process.stdout.isTTY;
92
- if (isInteractive) {
93
+ if (isInteractive() && !CI.isCI()) {
93
94
  const ok = await confirm(
94
95
  `About to apply ${unappliedMigrations.length} migration(s)\n` +
95
96
  "Your database may not be available to serve requests during the migration, continue?",
@@ -121,7 +122,7 @@ export const ApplyHandler = withConfig<BaseSqlExecuteArgs>(
121
122
  local,
122
123
  config,
123
124
  database,
124
- undefined,
125
+ isInteractive() && !CI.isCI(),
125
126
  persistTo,
126
127
  undefined,
127
128
  query
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "path";
3
3
  import { confirm } from "../../dialogs";
4
+ import { CI } from "../../is-ci";
5
+ import isInteractive from "../../is-interactive";
4
6
  import { logger } from "../../logger";
5
7
  import { DEFAULT_MIGRATION_PATH } from "../constants";
6
8
  import { executeSql } from "../execute";
@@ -79,7 +81,7 @@ const listAppliedMigrations = async (
79
81
  local,
80
82
  config,
81
83
  name,
82
- undefined,
84
+ isInteractive() && !CI.isCI(),
83
85
  persistTo,
84
86
  undefined,
85
87
  Query
@@ -130,7 +132,7 @@ export const initMigrationsTable = async (
130
132
  local,
131
133
  config,
132
134
  name,
133
- undefined,
135
+ isInteractive() && !CI.isCI(),
134
136
  persistTo,
135
137
  undefined,
136
138
  `
@@ -5,18 +5,18 @@ import React from "react";
5
5
  import { withConfig } from "../../config";
6
6
  import { logger } from "../../logger";
7
7
  import { requireAuth } from "../../user";
8
- import { Database } from "../options";
9
8
  import { d1BetaWarning, getDatabaseInfoFromConfig } from "../utils";
10
9
  import {
11
10
  getMigrationsPath,
12
11
  getUnappliedMigrations,
13
12
  initMigrationsTable,
14
13
  } from "./helpers";
14
+ import { DatabaseWithLocal } from "./options";
15
15
  import type { BaseSqlExecuteArgs } from "../execute";
16
16
  import type { Argv } from "yargs";
17
17
 
18
18
  export function ListOptions(yargs: Argv): Argv<BaseSqlExecuteArgs> {
19
- return Database(yargs);
19
+ return DatabaseWithLocal(yargs);
20
20
  }
21
21
 
22
22
  export const ListHandler = withConfig<BaseSqlExecuteArgs>(
@@ -0,0 +1,18 @@
1
+ import { Database } from "../options";
2
+ import type { Argv } from "yargs";
3
+
4
+ export function DatabaseWithLocal(yargs: Argv) {
5
+ return Database(yargs)
6
+ .option("local", {
7
+ describe:
8
+ "Execute commands/files against a local DB for use with wrangler dev --local",
9
+ type: "boolean",
10
+ })
11
+ .option("persist-to", {
12
+ describe:
13
+ "Specify directory to use for local persistence (you must use --local with this flag)",
14
+ type: "string",
15
+ requiresArg: true,
16
+ })
17
+ .implies("persist-to", "local");
18
+ }
package/src/dev/dev.tsx CHANGED
@@ -113,8 +113,8 @@ export type DevProps = {
113
113
  name: string | undefined;
114
114
  noBundle: boolean;
115
115
  entry: Entry;
116
- port: number;
117
- ip: string;
116
+ initialPort: number;
117
+ initialIp: string;
118
118
  inspectorPort: number;
119
119
  rules: Config["rules"];
120
120
  accountId: string | undefined;
@@ -171,25 +171,42 @@ export function DevImplementation(props: DevProps): JSX.Element {
171
171
  );
172
172
  }
173
173
 
174
+ // This is a nasty hack to allow `useHotkeys` and its "[b] open a browser" feature to read these values
175
+ // without triggering a re-render loop when `onReady()` updates them.
176
+ // The initially requested port can be different than what's actually used, if, for example, you request port 0.
177
+ let ip: string;
178
+ let port: number;
179
+
174
180
  function InteractiveDevSession(props: DevProps) {
175
181
  const toggles = useHotkeys({
176
182
  initial: {
177
183
  local: props.initialMode === "local",
178
184
  tunnel: false,
179
185
  },
180
- port: props.port,
181
- ip: props.ip,
182
186
  inspectorPort: props.inspectorPort,
183
187
  inspect: props.inspect,
184
188
  localProtocol: props.localProtocol,
185
189
  forceLocal: props.forceLocal,
186
190
  });
187
191
 
192
+ ip = props.initialIp;
193
+ port = props.initialPort;
194
+
188
195
  useTunnel(toggles.tunnel);
189
196
 
197
+ const onReady = (newIp: string, newPort: number) => {
198
+ if (newIp !== props.initialIp || newPort !== props.initialPort) {
199
+ ip = newIp;
200
+ port = newPort;
201
+ if (props.onReady) {
202
+ props.onReady(newIp, newPort);
203
+ }
204
+ }
205
+ };
206
+
190
207
  return (
191
208
  <>
192
- <DevSession {...props} local={toggles.local} />
209
+ <DevSession {...props} local={toggles.local} onReady={onReady} />
193
210
  <Box borderStyle="round" paddingLeft={1} paddingRight={1}>
194
211
  <Text bold={true}>[b]</Text>
195
212
  <Text> open a browser, </Text>
@@ -301,6 +318,22 @@ function DevSession(props: DevSessionProps) {
301
318
  );
302
319
  }
303
320
 
321
+ const announceAndOnReady: typeof props.onReady = (finalIp, finalPort) => {
322
+ if (process.send) {
323
+ process.send(
324
+ JSON.stringify({
325
+ event: "DEV_SERVER_READY",
326
+ ip: finalIp,
327
+ port: finalPort,
328
+ })
329
+ );
330
+ }
331
+
332
+ if (props.onReady) {
333
+ props.onReady(finalIp, finalPort);
334
+ }
335
+ };
336
+
304
337
  return props.local ? (
305
338
  <Local
306
339
  name={props.name}
@@ -312,8 +345,8 @@ function DevSession(props: DevSessionProps) {
312
345
  bindings={props.bindings}
313
346
  workerDefinitions={workerDefinitions}
314
347
  assetPaths={props.assetPaths}
315
- port={props.port}
316
- ip={props.ip}
348
+ initialPort={props.initialPort}
349
+ initialIp={props.initialIp}
317
350
  rules={props.rules}
318
351
  inspectorPort={props.inspectorPort}
319
352
  localPersistencePath={props.localPersistencePath}
@@ -324,7 +357,7 @@ function DevSession(props: DevSessionProps) {
324
357
  localUpstream={props.localUpstream}
325
358
  logPrefix={props.logPrefix}
326
359
  inspect={props.inspect}
327
- onReady={props.onReady}
360
+ onReady={announceAndOnReady}
328
361
  enablePagesAssetsServiceBinding={props.enablePagesAssetsServiceBinding}
329
362
  experimentalLocal={props.experimentalLocal}
330
363
  accountId={props.accountId}
@@ -339,8 +372,8 @@ function DevSession(props: DevSessionProps) {
339
372
  bindings={props.bindings}
340
373
  assetPaths={props.assetPaths}
341
374
  isWorkersSite={props.isWorkersSite}
342
- port={props.port}
343
- ip={props.ip}
375
+ port={props.initialPort}
376
+ ip={props.initialIp}
344
377
  localProtocol={props.localProtocol}
345
378
  inspectorPort={props.inspectorPort}
346
379
  // TODO: @threepointone #1167
@@ -354,7 +387,7 @@ function DevSession(props: DevSessionProps) {
354
387
  zone={props.zone}
355
388
  host={props.host}
356
389
  routes={props.routes}
357
- onReady={props.onReady}
390
+ onReady={announceAndOnReady}
358
391
  sourceMapPath={bundle?.sourceMapPath}
359
392
  sendMetrics={props.sendMetrics}
360
393
  />
@@ -503,25 +536,16 @@ type useHotkeysInitialState = {
503
536
  };
504
537
  function useHotkeys(props: {
505
538
  initial: useHotkeysInitialState;
506
- port: number;
507
- ip: string;
508
539
  inspectorPort: number;
509
540
  inspect: boolean;
510
541
  localProtocol: "http" | "https";
511
542
  forceLocal: boolean | undefined;
512
543
  }) {
513
- const {
514
- initial,
515
- port,
516
- ip,
517
- inspectorPort,
518
- inspect,
519
- localProtocol,
520
- forceLocal,
521
- } = props;
544
+ const { initial, inspectorPort, inspect, localProtocol, forceLocal } = props;
522
545
  // UGH, we should put port in context instead
523
546
  const [toggles, setToggles] = useState(initial);
524
547
  const { exit } = useApp();
548
+
525
549
  useInput(
526
550
  async (
527
551
  input,