wrangler 2.4.2 → 2.4.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wrangler",
3
- "version": "2.4.2",
3
+ "version": "2.4.3",
4
4
  "description": "Command-line interface for all things Cloudflare Workers",
5
5
  "keywords": [
6
6
  "wrangler",
@@ -34,7 +34,10 @@ describe("d1", () => {
34
34
  -h, --help Show help [boolean]
35
35
  -v, --version Show version number [boolean]
36
36
 
37
- 🚧 'wrangler d1 <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose"
37
+ 🚧 D1 is currently in open alpha and is not recommended for production data and traffic.
38
+ Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose.
39
+ To request features, visit https://community.cloudflare.com/c/developers/d1.
40
+ To give feedback, visit https://discord.gg/cloudflaredev"
38
41
  `);
39
42
  });
40
43
 
@@ -68,7 +71,10 @@ describe("d1", () => {
68
71
  -h, --help Show help [boolean]
69
72
  -v, --version Show version number [boolean]
70
73
 
71
- 🚧 'wrangler d1 <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose"
74
+ 🚧 D1 is currently in open alpha and is not recommended for production data and traffic.
75
+ Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose.
76
+ To request features, visit https://community.cloudflare.com/c/developers/d1.
77
+ To give feedback, visit https://discord.gg/cloudflaredev"
72
78
  `);
73
79
  });
74
80
  });
@@ -1,6 +1,11 @@
1
1
  import { type QueueResponse, type PostConsumerBody } from "../queues/client";
2
2
  import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
3
- import { setMockResponse, unsetAllMocks } from "./helpers/mock-cfetch";
3
+ import {
4
+ createFetchResult,
5
+ setMockRawResponse,
6
+ setMockResponse,
7
+ unsetAllMocks,
8
+ } from "./helpers/mock-cfetch";
4
9
  import { mockConsoleMethods } from "./helpers/mock-console";
5
10
  import { runInTempDir } from "./helpers/run-in-tmp";
6
11
  import { runWrangler } from "./helpers/run-wrangler";
@@ -157,6 +162,35 @@ describe("wrangler", () => {
157
162
  `);
158
163
  expect(requests.count).toEqual(1);
159
164
  });
165
+
166
+ it("should show link to dash when not enabled", async () => {
167
+ const queueName = "testQueue";
168
+ setMockRawResponse(
169
+ "/accounts/:accountId/workers/queues",
170
+ ([_url, accountId]) => {
171
+ expect(accountId).toEqual("some-account-id");
172
+ return createFetchResult(null, false, [
173
+ { message: "workers.api.error.unauthorized", code: 10023 },
174
+ ]);
175
+ }
176
+ );
177
+ await expect(
178
+ runWrangler(`queues create ${queueName}`)
179
+ ).rejects.toThrowError();
180
+ expect(std.out).toMatchInlineSnapshot(`
181
+ "Creating queue testQueue.
182
+ Queues is not currently enabled on this account. Go to https://dash.cloudflare.com/some-account-id/workers/queues to enable it.
183
+
184
+ X [ERROR] A request to the Cloudflare API (/accounts/some-account-id/workers/queues) failed.
185
+
186
+ workers.api.error.unauthorized [code: 10023]
187
+
188
+ If you think this is a bug, please open an issue at:
189
+ https://github.com/cloudflare/wrangler2/issues/new/choose
190
+
191
+ "
192
+ `);
193
+ });
160
194
  });
161
195
 
162
196
  describe("delete", () => {
@@ -309,6 +343,35 @@ describe("wrangler", () => {
309
343
  Added consumer to queue testQueue."
310
344
  `);
311
345
  });
346
+
347
+ it("should show link to dash when not enabled", async () => {
348
+ const queueName = "testQueue";
349
+ setMockRawResponse(
350
+ `/accounts/:accountId/workers/queues/${queueName}/consumers`,
351
+ ([_url, accountId]) => {
352
+ expect(accountId).toEqual("some-account-id");
353
+ return createFetchResult(null, false, [
354
+ { message: "workers.api.error.unauthorized", code: 10023 },
355
+ ]);
356
+ }
357
+ );
358
+ await expect(
359
+ runWrangler(`queues consumer add ${queueName} testScript`)
360
+ ).rejects.toThrowError();
361
+ expect(std.out).toMatchInlineSnapshot(`
362
+ "Adding consumer to queue testQueue.
363
+ Queues is not currently enabled on this account. Go to https://dash.cloudflare.com/some-account-id/workers/queues to enable it.
364
+
365
+ X [ERROR] A request to the Cloudflare API (/accounts/some-account-id/workers/queues/testQueue/consumers) failed.
366
+
367
+ workers.api.error.unauthorized [code: 10023]
368
+
369
+ If you think this is a bug, please open an issue at:
370
+ https://github.com/cloudflare/wrangler2/issues/new/choose
371
+
372
+ "
373
+ `);
374
+ });
312
375
  });
313
376
 
314
377
  describe("delete", () => {
package/src/bundle.ts CHANGED
@@ -28,6 +28,19 @@ type StaticAssetsConfig =
28
28
  })
29
29
  | undefined;
30
30
 
31
+ /**
32
+ * When applying the middleware facade for service workers, we need to inject
33
+ * some code at the top of the final output bundle. Applying an inject too early
34
+ * will allow esbuild to reorder the code. Additionally, we need to make sure
35
+ * user code is bundled in the final esbuild step with `watch` correctly
36
+ * configured, so code changes are detected.
37
+ *
38
+ * This type is used as the return type for the `MiddlewareFn` type representing
39
+ * a facade-applying function. Returned injects should be injected with the
40
+ * final esbuild step.
41
+ */
42
+ type EntryWithInject = Entry & { inject?: string[] };
43
+
31
44
  /**
32
45
  * RegExp matching against esbuild's error text when it is unable to resolve
33
46
  * a Node built-in module. If we detect this when node_compat is disabled,
@@ -97,6 +110,7 @@ export async function bundleWorker(
97
110
  targetConsumer: "dev" | "publish";
98
111
  local: boolean;
99
112
  testScheduled?: boolean;
113
+ experimentalLocal?: boolean;
100
114
  inject?: string[];
101
115
  loader?: Record<string, string>;
102
116
  sourcemap?: esbuild.CommonOptions["sourcemap"];
@@ -124,6 +138,7 @@ export async function bundleWorker(
124
138
  firstPartyWorkerDevFacade,
125
139
  targetConsumer,
126
140
  testScheduled,
141
+ experimentalLocal,
127
142
  inject: injectOption,
128
143
  loader,
129
144
  sourcemap,
@@ -197,8 +212,29 @@ export async function bundleWorker(
197
212
  path: "templates/middleware/middleware-scheduled.ts",
198
213
  });
199
214
  }
215
+ if (experimentalLocal) {
216
+ // In Miniflare 3, we bind the user's worker as a service binding in a
217
+ // special entry worker that handles things like injecting `Request.cf`,
218
+ // live-reload, and the pretty-error page.
219
+ //
220
+ // Unfortunately, due to a bug in `workerd`, errors thrown asynchronously by
221
+ // native APIs don't have `stack`s. This means Miniflare can't extract the
222
+ // `stack` trace from dispatching to the user worker service binding by
223
+ // `try/catch`.
224
+ //
225
+ // As a stop-gap solution, if the `MF-Experimental-Error-Stack` header is
226
+ // truthy on responses, the body will be interpreted as a JSON-error of the
227
+ // form `{ message?: string, name?: string, stack?: string }`.
228
+ //
229
+ // This middleware wraps the user's worker in a `try/catch`, and rewrites
230
+ // errors in this format so a pretty-error page can be shown.
231
+ middlewareToLoad.push({
232
+ path: "templates/middleware/middleware-miniflare3-json-error.ts",
233
+ dev: true,
234
+ });
235
+ }
200
236
 
201
- type MiddlewareFn = (arg0: Entry) => Promise<Entry>;
237
+ type MiddlewareFn = (currentEntry: Entry) => Promise<EntryWithInject>;
202
238
  const middleware: (false | undefined | MiddlewareFn)[] = [
203
239
  // serve static assets
204
240
  serveAssetsFromWorker &&
@@ -259,23 +295,22 @@ export async function bundleWorker(
259
295
  (m) =>
260
296
  (targetConsumer === "dev" && m.dev !== false) ||
261
297
  (m.publish && targetConsumer === "publish")
262
- ),
263
- moduleCollector.plugin
298
+ )
264
299
  );
265
300
  }),
266
301
  ].filter(Boolean);
267
302
 
268
- let inputEntry = entry;
303
+ const inject: string[] = injectOption ?? [];
304
+ if (checkFetch) inject.push(checkedFetchFileToInject);
269
305
 
306
+ let inputEntry: EntryWithInject = entry;
270
307
  for (const middlewareFn of middleware as MiddlewareFn[]) {
271
308
  inputEntry = await middlewareFn(inputEntry);
309
+ if (inputEntry.inject !== undefined) inject.push(...inputEntry.inject);
272
310
  }
273
311
 
274
312
  // At this point, inputEntry points to the entry point we want to build.
275
313
 
276
- const inject: string[] = injectOption ?? [];
277
- if (checkFetch) inject.push(checkedFetchFileToInject);
278
-
279
314
  const buildOptions: esbuild.BuildOptions & { metafile: true } = {
280
315
  entryPoints: [inputEntry.file],
281
316
  bundle: true,
@@ -315,11 +350,7 @@ export async function bundleWorker(
315
350
  ...(loader || {}),
316
351
  },
317
352
  plugins: [
318
- // We run the moduleCollector plugin for service workers as part of the middleware loader
319
- // so we only run here for modules or with no middleware to load
320
- ...(entry.format === "modules" || middlewareToLoad.length === 0
321
- ? [moduleCollector.plugin]
322
- : []),
353
+ moduleCollector.plugin,
323
354
  ...(nodeCompat
324
355
  ? [NodeGlobalsPolyfills({ buffer: true }), NodeModulesPolyfills()]
325
356
  : []),
@@ -456,14 +487,16 @@ interface MiddlewareLoader {
456
487
  async function applyMiddlewareLoaderFacade(
457
488
  entry: Entry,
458
489
  tmpDirPath: string,
459
- middleware: MiddlewareLoader[], // a list of paths to middleware files
460
- moduleCollectorPlugin: esbuild.Plugin
461
- ): Promise<Entry> {
490
+ middleware: MiddlewareLoader[] // a list of paths to middleware files
491
+ ): Promise<EntryWithInject> {
462
492
  // Firstly we need to insert the middleware array into the project,
463
493
  // and then we load the middleware - this insertion and loading is
464
494
  // different for each format.
465
495
 
466
- // STEP 1: Insert the middleware
496
+ // Make sure we resolve all files relative to the actual temporary directory,
497
+ // otherwise we'll have issues with source maps
498
+ tmpDirPath = fs.realpathSync(tmpDirPath);
499
+
467
500
  const targetPathInsertion = path.join(
468
501
  tmpDirPath,
469
502
  "middleware-insertion.entry.js"
@@ -506,7 +539,7 @@ async function applyMiddlewareLoaderFacade(
506
539
  );
507
540
 
508
541
  await esbuild.build({
509
- entryPoints: [path.resolve(getBasePath(), dynamicFacadePath)],
542
+ entryPoints: [dynamicFacadePath],
510
543
  bundle: true,
511
544
  sourcemap: true,
512
545
  format: "esm",
@@ -523,98 +556,80 @@ async function applyMiddlewareLoaderFacade(
523
556
  ],
524
557
  outfile: targetPathInsertion,
525
558
  });
526
- } else {
527
- // We handle service workers slightly differently as we have to overwrite
528
- // the event listeners and reimplement them
529
559
 
560
+ let targetPathLoader = path.join(tmpDirPath, path.basename(entry.file));
561
+ if (path.extname(entry.file) === "") targetPathLoader += ".js";
562
+ const loaderPath = path.resolve(
563
+ getBasePath(),
564
+ "templates/middleware/loader-modules.ts"
565
+ );
530
566
  await esbuild.build({
531
- entryPoints: [entry.file],
567
+ entryPoints: [loaderPath],
532
568
  bundle: true,
533
569
  sourcemap: true,
534
- define: {
535
- "process.env.NODE_ENV": `"${process.env["NODE_ENV" + ""]}"`,
536
- },
537
570
  format: "esm",
538
- outfile: targetPathInsertion,
539
- plugins: [moduleCollectorPlugin],
571
+ plugins: [
572
+ esbuildAliasExternalPlugin({
573
+ __ENTRY_POINT__: targetPathInsertion,
574
+ "./common": path.resolve(
575
+ getBasePath(),
576
+ "templates/middleware/common.ts"
577
+ ),
578
+ }),
579
+ ],
580
+ outfile: targetPathLoader,
540
581
  });
541
-
582
+ return {
583
+ ...entry,
584
+ file: targetPathLoader,
585
+ };
586
+ } else {
542
587
  const imports = middlewareIdentifiers
543
- .map(
544
- (m, i) =>
545
- `import ${m} from "${toUrlPath(
546
- path.resolve(getBasePath(), middleware[i].path)
547
- )}";`
548
- )
588
+ .map((m) => `import ${m} from "${m}";`)
549
589
  .join("\n");
550
-
551
- // We add the new modules with imports and then register using the
552
- // addMiddleware function (which gets rewritten in the next build step)
553
-
554
- // We choose to run middleware inserted in wrangler before user inserted
555
- // middleware in the stack
556
- // To do this, we either need to execute the addMiddleware function first
557
- // before any user middleware, or use a separate handling function.
558
- // We choose to do the latter as to prepend, we would have to load the entire
559
- // script into memory as a prepend function doesn't exist or work in the same
560
- // way that an append function does.
561
-
562
- fs.copyFileSync(targetPathInsertion, dynamicFacadePath);
563
- fs.appendFileSync(
564
- dynamicFacadePath,
565
- `
590
+ const contents = `import { __facade_registerInternal__ } from "__LOADER__";
566
591
  ${imports}
567
- addMiddlewareInternal([${middlewareIdentifiers.join(",")}])
568
- `
569
- );
570
- }
571
-
572
- // STEP 2: Load the middleware
573
- // We want to get the filename of the orginal entry point
574
- let targetPathLoader = path.join(tmpDirPath, path.basename(entry.file));
575
- if (path.extname(entry.file) === "") targetPathLoader += ".js";
576
-
577
- const loaderPath =
578
- entry.format === "modules"
579
- ? path.resolve(getBasePath(), "templates/middleware/loader-modules.ts")
580
- : dynamicFacadePath;
592
+ __facade_registerInternal__([${middlewareIdentifiers.join(",")}]);`;
593
+ fs.writeFileSync(dynamicFacadePath, contents);
581
594
 
582
- await esbuild.build({
583
- entryPoints: [loaderPath],
584
- bundle: true,
585
- sourcemap: true,
586
- format: "esm",
587
- ...(entry.format === "service-worker"
588
- ? {
589
- inject: [
590
- path.resolve(getBasePath(), "templates/middleware/loader-sw.ts"),
591
- ],
592
- define: {
593
- addEventListener: "__facade_addEventListener__",
594
- removeEventListener: "__facade_removeEventListener__",
595
- dispatchEvent: "__facade_dispatchEvent__",
596
- addMiddleware: "__facade_register__",
597
- addMiddlewareInternal: "__facade_registerInternal__",
598
- },
599
- }
600
- : {
601
- plugins: [
602
- esbuildAliasExternalPlugin({
603
- __ENTRY_POINT__: targetPathInsertion,
604
- "./common": path.resolve(
595
+ await esbuild.build({
596
+ entryPoints: [dynamicFacadePath],
597
+ bundle: true,
598
+ sourcemap: true,
599
+ format: "iife",
600
+ plugins: [
601
+ {
602
+ name: "dynamic-facade-imports",
603
+ setup(build) {
604
+ build.onResolve({ filter: /^__LOADER__$/ }, () => {
605
+ const loaderPath = path.resolve(
605
606
  getBasePath(),
606
- "templates/middleware/common.ts"
607
- ),
608
- }),
609
- ],
610
- }),
611
- outfile: targetPathLoader,
612
- });
613
-
614
- return {
615
- ...entry,
616
- file: targetPathLoader,
617
- };
607
+ "templates/middleware/loader-sw.ts"
608
+ );
609
+ return { path: loaderPath };
610
+ });
611
+ const middlewareFilter = /^__MIDDLEWARE_(\d+)__$/;
612
+ build.onResolve({ filter: middlewareFilter }, (args) => {
613
+ const match = middlewareFilter.exec(args.path);
614
+ assert(match !== null);
615
+ const middlewareIndex = parseInt(match[1]);
616
+ return {
617
+ path: path.resolve(
618
+ getBasePath(),
619
+ middleware[middlewareIndex].path
620
+ ),
621
+ };
622
+ });
623
+ },
624
+ },
625
+ ],
626
+ outfile: targetPathInsertion,
627
+ });
628
+ return {
629
+ ...entry,
630
+ inject: [targetPathInsertion],
631
+ };
632
+ }
618
633
  }
619
634
 
620
635
  /**
@@ -1814,7 +1814,7 @@ const validateD1Binding: ValidatorFn = (diagnostics, field, value) => {
1814
1814
  }
1815
1815
  if (isValid && !process.env.NO_D1_WARNING) {
1816
1816
  diagnostics.warnings.push(
1817
- `D1 Bindings are currently in beta to allow the API to evolve before general availability.\nPlease report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose\nNote: set NO_D1_WARNING=true to hide this message`
1817
+ `D1 Bindings are currently in alpha to allow the API to evolve before general availability.\nPlease report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose\nNote: set NO_D1_WARNING=true to hide this message`
1818
1818
  );
1819
1819
  }
1820
1820
  return isValid;
@@ -0,0 +1,183 @@
1
+ import fs from "node:fs";
2
+ import path from "path";
3
+ import { Box, render, Text } from "ink";
4
+ import Table from "ink-table";
5
+ import React from "react";
6
+ import { withConfig } from "../../config";
7
+ import { confirm } from "../../dialogs";
8
+ import { logger } from "../../logger";
9
+ import { requireAuth } from "../../user";
10
+ import { createBackup } from "../backups";
11
+ import { executeSql } from "../execute";
12
+ import { Database } from "../options";
13
+ import { d1BetaWarning, getDatabaseInfoFromConfig } from "../utils";
14
+ import {
15
+ getMigrationsPath,
16
+ getUnappliedMigrations,
17
+ initMigrationsTable,
18
+ } from "./helpers";
19
+ import type { ParseError } from "../../parse";
20
+ import type { BaseSqlExecuteArgs } from "../execute";
21
+ import type { Argv } from "yargs";
22
+
23
+ export function ApplyOptions(yargs: Argv): Argv<BaseSqlExecuteArgs> {
24
+ return Database(yargs);
25
+ }
26
+
27
+ export const ApplyHandler = withConfig<BaseSqlExecuteArgs>(
28
+ async ({ config, database, local, persistTo }): Promise<void> => {
29
+ const accountId = await requireAuth({});
30
+ logger.log(d1BetaWarning);
31
+
32
+ const databaseInfo = await getDatabaseInfoFromConfig(config, database);
33
+ if (!databaseInfo) {
34
+ throw new Error(
35
+ `Can't find a DB with name/binding '${database}' in local config. Check info in wrangler.toml...`
36
+ );
37
+ }
38
+
39
+ if (!config.configPath) {
40
+ return;
41
+ }
42
+
43
+ const migrationsPath = await getMigrationsPath(
44
+ path.dirname(config.configPath),
45
+ databaseInfo.migrationsFolderPath,
46
+ false
47
+ );
48
+ await initMigrationsTable(
49
+ databaseInfo.migrationsTableName,
50
+ local,
51
+ config,
52
+ database,
53
+ persistTo
54
+ );
55
+
56
+ const unappliedMigrations = (
57
+ await getUnappliedMigrations(
58
+ databaseInfo.migrationsTableName,
59
+ migrationsPath,
60
+ local,
61
+ config,
62
+ database,
63
+ persistTo
64
+ )
65
+ )
66
+ .map((migration) => {
67
+ return {
68
+ Name: migration,
69
+ Status: "🕒️",
70
+ };
71
+ })
72
+ .sort((a, b) => {
73
+ const migrationNumberA = parseInt(a.Name.split("_")[0]);
74
+ const migrationNumberB = parseInt(b.Name.split("_")[0]);
75
+ if (migrationNumberA < migrationNumberB) {
76
+ return -1;
77
+ }
78
+ if (migrationNumberA > migrationNumberB) {
79
+ return 1;
80
+ }
81
+
82
+ // numbers must be equal
83
+ return 0;
84
+ });
85
+
86
+ if (unappliedMigrations.length === 0) {
87
+ render(<Text>✅ No migrations to apply!</Text>);
88
+ return;
89
+ }
90
+
91
+ const isInteractive = process.stdout.isTTY;
92
+ if (isInteractive) {
93
+ const ok = await confirm(
94
+ `About to apply ${unappliedMigrations.length} migration(s)\n` +
95
+ "Your database may not be available to serve requests during the migration, continue?",
96
+ <Box flexDirection="column">
97
+ <Text>Migrations to be applied:</Text>
98
+ <Table data={unappliedMigrations} columns={["Name"]}></Table>
99
+ </Box>
100
+ );
101
+ if (!ok) return;
102
+ }
103
+
104
+ render(<Text>🕒 Creating backup...</Text>);
105
+ await createBackup(accountId, databaseInfo.uuid);
106
+
107
+ for (const migration of unappliedMigrations) {
108
+ let query = fs.readFileSync(
109
+ `${migrationsPath}/${migration.Name}`,
110
+ "utf8"
111
+ );
112
+ query += `
113
+ INSERT INTO ${databaseInfo.migrationsTableName} (name)
114
+ values ('${migration.Name}');
115
+ `;
116
+
117
+ let success = true;
118
+ let errorNotes: Array<string> = [];
119
+ try {
120
+ const response = await executeSql(
121
+ local,
122
+ config,
123
+ database,
124
+ undefined,
125
+ persistTo,
126
+ undefined,
127
+ query
128
+ );
129
+
130
+ if (response === null) {
131
+ // TODO: return error
132
+ return;
133
+ }
134
+
135
+ for (const result of response) {
136
+ // When executing more than 1 statement, response turns into an array of QueryResult
137
+ if (Array.isArray(result)) {
138
+ for (const subResult of result) {
139
+ if (!subResult.success) {
140
+ success = false;
141
+ }
142
+ }
143
+ } else {
144
+ if (!result.success) {
145
+ success = false;
146
+ }
147
+ }
148
+ }
149
+ } catch (e) {
150
+ const err = e as ParseError;
151
+
152
+ success = false;
153
+ errorNotes = err.notes.map((msg) => msg.text);
154
+ }
155
+
156
+ migration.Status = success ? "✅" : "❌";
157
+
158
+ render(
159
+ <Box flexDirection="column">
160
+ <Table
161
+ data={unappliedMigrations}
162
+ columns={["Name", "Status"]}
163
+ ></Table>
164
+ {errorNotes.length > 0 && (
165
+ <Box flexDirection="column">
166
+ <Text>&nbsp;</Text>
167
+ <Text>
168
+ ❌ Migration {migration.Name} failed with following Errors
169
+ </Text>
170
+ <Table
171
+ data={errorNotes.map((err) => {
172
+ return { Error: err };
173
+ })}
174
+ ></Table>
175
+ </Box>
176
+ )}
177
+ </Box>
178
+ );
179
+
180
+ if (errorNotes.length > 0) return;
181
+ }
182
+ }
183
+ );
@@ -0,0 +1,77 @@
1
+ import fs from "node:fs";
2
+ import path from "path";
3
+ import { Box, render, Text } from "ink";
4
+ import React from "react";
5
+ import { withConfig } from "../../config";
6
+ import { logger } from "../../logger";
7
+ import { requireAuth } from "../../user";
8
+ import { Database } from "../options";
9
+ import { d1BetaWarning, getDatabaseInfoFromConfig } from "../utils";
10
+ import { getMigrationsPath, getNextMigrationNumber } from "./helpers";
11
+ import type { Argv } from "yargs";
12
+
13
+ type MigrationsCreateArgs = {
14
+ config?: string;
15
+ database: string;
16
+ message: string;
17
+ };
18
+
19
+ export function CreateOptions(yargs: Argv): Argv<MigrationsCreateArgs> {
20
+ return Database(yargs).positional("message", {
21
+ describe: "The Migration message",
22
+ type: "string",
23
+ demandOption: true,
24
+ });
25
+ }
26
+
27
+ export const CreateHandler = withConfig<MigrationsCreateArgs>(
28
+ async ({ config, database, message }): Promise<void> => {
29
+ await requireAuth({});
30
+ logger.log(d1BetaWarning);
31
+
32
+ const databaseInfo = await getDatabaseInfoFromConfig(config, database);
33
+ if (!databaseInfo) {
34
+ throw new Error(
35
+ `Can't find a DB with name/binding '${database}' in local config. Check info in wrangler.toml...`
36
+ );
37
+ }
38
+
39
+ if (!config.configPath) {
40
+ return;
41
+ }
42
+
43
+ const migrationsPath = await getMigrationsPath(
44
+ path.dirname(config.configPath),
45
+ databaseInfo.migrationsFolderPath,
46
+ true
47
+ );
48
+ const nextMigrationNumber = pad(getNextMigrationNumber(migrationsPath), 4);
49
+ const migrationName = message.replaceAll(" ", "_");
50
+
51
+ const newMigrationName = `${nextMigrationNumber}_${migrationName}.sql`;
52
+
53
+ fs.writeFileSync(
54
+ `${migrationsPath}/${newMigrationName}`,
55
+ `-- Migration number: ${nextMigrationNumber} \t ${new Date().toISOString()}\n`
56
+ );
57
+
58
+ render(
59
+ <Box flexDirection="column">
60
+ <Text>
61
+ ✅ Successfully created Migration &apos;{newMigrationName}&apos;!
62
+ </Text>
63
+ <Text>&nbsp;</Text>
64
+ <Text>The migration is available for editing here</Text>
65
+ <Text>
66
+ {migrationsPath}/{newMigrationName}
67
+ </Text>
68
+ </Box>
69
+ );
70
+ }
71
+ );
72
+
73
+ function pad(num: number, size: number): string {
74
+ let newNum = num.toString();
75
+ while (newNum.length < size) newNum = "0" + newNum;
76
+ return newNum;
77
+ }