wrangler 2.0.24 → 2.0.27

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 (63) hide show
  1. package/miniflare-dist/index.mjs +142 -16
  2. package/package.json +3 -3
  3. package/src/__tests__/configuration.test.ts +7 -3
  4. package/src/__tests__/dev.test.tsx +26 -4
  5. package/src/__tests__/generate.test.ts +2 -4
  6. package/src/__tests__/helpers/mock-cfetch.ts +35 -2
  7. package/src/__tests__/init.test.ts +537 -359
  8. package/src/__tests__/jest.setup.ts +7 -0
  9. package/src/__tests__/metrics.test.ts +1 -1
  10. package/src/__tests__/pages.test.ts +14 -0
  11. package/src/__tests__/r2.test.ts +22 -3
  12. package/src/__tests__/tail.test.ts +112 -42
  13. package/src/__tests__/user.test.ts +11 -0
  14. package/src/api/dev.ts +7 -0
  15. package/src/bundle.ts +3 -2
  16. package/src/cfetch/internal.ts +56 -0
  17. package/src/config/config.ts +1 -1
  18. package/src/config/validation-helpers.ts +19 -6
  19. package/src/config/validation.ts +9 -3
  20. package/src/config-cache.ts +2 -1
  21. package/src/dev/dev.tsx +16 -2
  22. package/src/dev/local.tsx +69 -5
  23. package/src/dev/use-esbuild.ts +3 -0
  24. package/src/dev-registry.tsx +3 -0
  25. package/src/dev.tsx +28 -19
  26. package/src/generate.ts +1 -1
  27. package/src/index.tsx +51 -21
  28. package/src/init.ts +111 -38
  29. package/src/inspect.ts +1 -4
  30. package/src/{metrics/is-ci.ts → is-ci.ts} +0 -0
  31. package/src/metrics/metrics-config.ts +1 -1
  32. package/src/miniflare-cli/assets.ts +27 -16
  33. package/src/miniflare-cli/index.ts +124 -2
  34. package/src/pages/build.tsx +75 -41
  35. package/src/pages/constants.ts +4 -0
  36. package/src/pages/deployments.tsx +9 -9
  37. package/src/pages/dev.tsx +178 -64
  38. package/src/pages/errors.ts +22 -0
  39. package/src/pages/functions/buildPlugin.ts +4 -0
  40. package/src/pages/functions/buildWorker.ts +4 -0
  41. package/src/pages/functions/routes-consolidation.test.ts +250 -0
  42. package/src/pages/functions/routes-consolidation.ts +73 -0
  43. package/src/pages/functions/routes-transformation.test.ts +271 -0
  44. package/src/pages/functions/routes-transformation.ts +122 -0
  45. package/src/pages/functions.tsx +96 -0
  46. package/src/pages/index.tsx +65 -55
  47. package/src/pages/projects.tsx +9 -3
  48. package/src/pages/publish.tsx +75 -22
  49. package/src/pages/types.ts +9 -0
  50. package/src/pages/upload.tsx +6 -8
  51. package/src/proxy.ts +10 -0
  52. package/src/r2.ts +17 -4
  53. package/src/tail/filters.ts +3 -1
  54. package/src/tail/index.ts +15 -2
  55. package/src/tail/printing.ts +43 -3
  56. package/src/user/user.tsx +6 -4
  57. package/src/whoami.tsx +5 -5
  58. package/templates/pages-template-plugin.ts +16 -4
  59. package/templates/pages-template-worker.ts +16 -5
  60. package/templates/service-bindings-module-facade.js +10 -7
  61. package/templates/service-bindings-sw-facade.js +10 -7
  62. package/wrangler-dist/cli.d.ts +7 -0
  63. package/wrangler-dist/cli.js +1681 -1091
package/src/init.ts CHANGED
@@ -5,12 +5,16 @@ import TOML from "@iarna/toml";
5
5
  import { findUp } from "find-up";
6
6
  import { version as wranglerVersion } from "../package.json";
7
7
 
8
+ import { fetchDashboardScript } from "./cfetch/internal";
9
+ import { readConfig } from "./config";
8
10
  import { confirm, select } from "./dialogs";
9
11
  import { initializeGit, isGitInstalled, isInsideGitRepo } from "./git-client";
10
12
  import { logger } from "./logger";
11
13
  import { getPackageManager } from "./package-manager";
12
14
  import { parsePackageJSON, parseTOML, readFileSync } from "./parse";
15
+ import { requireAuth } from "./user";
13
16
  import { CommandLineArgsError, printWranglerBanner } from "./index";
17
+ import type { ConfigPath } from "./index";
14
18
 
15
19
  import type { Argv, ArgumentsCamelCase } from "yargs";
16
20
 
@@ -36,6 +40,12 @@ export async function initOptions(yargs: Argv) {
36
40
  describe: 'Answer "yes" to any prompts for new projects',
37
41
  type: "boolean",
38
42
  alias: "y",
43
+ })
44
+ .option("from-dash", {
45
+ describe: "Download script from the dashboard for local development",
46
+ type: "string",
47
+ requiresArg: true,
48
+ hidden: true,
39
49
  });
40
50
  }
41
51
 
@@ -61,7 +71,11 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
61
71
  const devDepsToInstall: string[] = [];
62
72
  const instructions: string[] = [];
63
73
  let shouldRunPackageManagerInstall = false;
64
- const creationDirectory = path.resolve(process.cwd(), args.name ?? "");
74
+ const fromDashScriptName = args["from-dash"] as string;
75
+ const creationDirectory = path.resolve(
76
+ process.cwd(),
77
+ (args.name ? args.name : fromDashScriptName) ?? ""
78
+ );
65
79
 
66
80
  if (args.site) {
67
81
  const gitDirectory =
@@ -90,6 +104,7 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
90
104
 
91
105
  // TODO: ask which directory to make the worker in (defaults to args.name)
92
106
  // TODO: if args.name isn't provided, ask what to name the worker
107
+ // Note: `--from-dash` will be a fallback creationDir/Worker name if none is provided.
93
108
 
94
109
  const wranglerTomlDestination = path.join(
95
110
  creationDirectory,
@@ -98,12 +113,15 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
98
113
  let justCreatedWranglerToml = false;
99
114
 
100
115
  if (fs.existsSync(wranglerTomlDestination)) {
116
+ let shouldContinue = false;
101
117
  logger.warn(
102
118
  `${path.relative(process.cwd(), wranglerTomlDestination)} already exists!`
103
119
  );
104
- const shouldContinue = await confirm(
105
- "Do you want to continue initializing this project?"
106
- );
120
+ if (!fromDashScriptName) {
121
+ shouldContinue = await confirm(
122
+ "Do you want to continue initializing this project?"
123
+ );
124
+ }
107
125
  if (!shouldContinue) {
108
126
  return;
109
127
  }
@@ -438,27 +456,23 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
438
456
  process.cwd(),
439
457
  path.join(creationDirectory, "./src/index.ts")
440
458
  );
441
-
442
- const newWorkerType = yesFlag
443
- ? "fetch"
444
- : await getNewWorkerType(newWorkerFilename);
445
-
446
- if (newWorkerType !== "none") {
447
- const template = getNewWorkerTemplate("ts", newWorkerType);
448
-
459
+ if (fromDashScriptName) {
460
+ const config = readConfig(args.config as ConfigPath, args);
461
+ const accountId = await requireAuth(config);
449
462
  await mkdir(path.join(creationDirectory, "./src"), {
450
463
  recursive: true,
451
464
  });
452
- await writeFile(
453
- path.join(creationDirectory, "./src/index.ts"),
454
- readFileSync(path.join(__dirname, `../templates/${template}`))
465
+
466
+ const dashScript = await fetchDashboardScript(
467
+ `/accounts/${accountId}/workers/scripts/${fromDashScriptName}`,
468
+ {
469
+ method: "GET",
470
+ }
455
471
  );
456
472
 
457
- logger.log(
458
- `✨ Created ${path.relative(
459
- process.cwd(),
460
- path.join(creationDirectory, "./src/index.ts")
461
- )}`
473
+ await writeFile(
474
+ path.join(creationDirectory, "./src/index.ts"),
475
+ dashScript
462
476
  );
463
477
 
464
478
  await writePackageJsonScriptsAndUpdateWranglerToml(
@@ -466,8 +480,39 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
466
480
  justCreatedWranglerToml,
467
481
  pathToPackageJson,
468
482
  "src/index.ts",
469
- getNewWorkerToml(newWorkerType)
483
+ {}
470
484
  );
485
+ } else {
486
+ const newWorkerType = yesFlag
487
+ ? "fetch"
488
+ : await getNewWorkerType(newWorkerFilename);
489
+
490
+ if (newWorkerType !== "none") {
491
+ const template = getNewWorkerTemplate("ts", newWorkerType);
492
+
493
+ await mkdir(path.join(creationDirectory, "./src"), {
494
+ recursive: true,
495
+ });
496
+ await writeFile(
497
+ path.join(creationDirectory, "./src/index.ts"),
498
+ readFileSync(path.join(__dirname, `../templates/${template}`))
499
+ );
500
+
501
+ logger.log(
502
+ `✨ Created ${path.relative(
503
+ process.cwd(),
504
+ path.join(creationDirectory, "./src/index.ts")
505
+ )}`
506
+ );
507
+
508
+ await writePackageJsonScriptsAndUpdateWranglerToml(
509
+ shouldWritePackageJsonScripts,
510
+ justCreatedWranglerToml,
511
+ pathToPackageJson,
512
+ "src/index.ts",
513
+ getNewWorkerToml(newWorkerType)
514
+ );
515
+ }
471
516
  }
472
517
  }
473
518
  } else {
@@ -477,35 +522,63 @@ export async function initHandler(args: ArgumentsCamelCase<InitArgs>) {
477
522
  path.join(creationDirectory, "./src/index.js")
478
523
  );
479
524
 
480
- const newWorkerType = yesFlag
481
- ? "fetch"
482
- : await getNewWorkerType(newWorkerFilename);
483
-
484
- if (newWorkerType !== "none") {
485
- const template = getNewWorkerTemplate("js", newWorkerType);
486
-
525
+ if (fromDashScriptName) {
526
+ const config = readConfig(args.config as ConfigPath, args);
527
+ const accountId = await requireAuth(config);
487
528
  await mkdir(path.join(creationDirectory, "./src"), {
488
529
  recursive: true,
489
530
  });
490
- await writeFile(
491
- path.join(creationDirectory, "./src/index.js"),
492
- readFileSync(path.join(__dirname, `../templates/${template}`))
531
+
532
+ const dashScript = await fetchDashboardScript(
533
+ `/accounts/${accountId}/workers/scripts/${fromDashScriptName}`,
534
+ {
535
+ method: "GET",
536
+ }
493
537
  );
494
538
 
495
- logger.log(
496
- `✨ Created ${path.relative(
497
- process.cwd(),
498
- path.join(creationDirectory, "./src/index.js")
499
- )}`
539
+ await writeFile(
540
+ path.join(creationDirectory, "./src/index.js"),
541
+ dashScript
500
542
  );
501
543
 
502
544
  await writePackageJsonScriptsAndUpdateWranglerToml(
503
545
  shouldWritePackageJsonScripts,
504
546
  justCreatedWranglerToml,
505
547
  pathToPackageJson,
506
- "src/index.js",
507
- getNewWorkerToml(newWorkerType)
548
+ "src/index.ts",
549
+ {}
508
550
  );
551
+ } else {
552
+ const newWorkerType = yesFlag
553
+ ? "fetch"
554
+ : await getNewWorkerType(newWorkerFilename);
555
+
556
+ if (newWorkerType !== "none") {
557
+ const template = getNewWorkerTemplate("js", newWorkerType);
558
+
559
+ await mkdir(path.join(creationDirectory, "./src"), {
560
+ recursive: true,
561
+ });
562
+ await writeFile(
563
+ path.join(creationDirectory, "./src/index.js"),
564
+ readFileSync(path.join(__dirname, `../templates/${template}`))
565
+ );
566
+
567
+ logger.log(
568
+ `✨ Created ${path.relative(
569
+ process.cwd(),
570
+ path.join(creationDirectory, "./src/index.js")
571
+ )}`
572
+ );
573
+
574
+ await writePackageJsonScriptsAndUpdateWranglerToml(
575
+ shouldWritePackageJsonScripts,
576
+ justCreatedWranglerToml,
577
+ pathToPackageJson,
578
+ "src/index.js",
579
+ getNewWorkerToml(newWorkerType)
580
+ );
581
+ }
509
582
  }
510
583
  }
511
584
  }
package/src/inspect.ts CHANGED
@@ -320,10 +320,7 @@ export default function useInspector(props: InspectorProps) {
320
320
  const convertedFnName =
321
321
  pos.name || functionName || "";
322
322
  exceptionLines.push(
323
- ` at ${convertedFnName} (${pos.source?.replace(
324
- `${mapContent.sourceRoot}/`,
325
- ""
326
- )}:${pos.line}:${pos.column})`
323
+ ` at ${convertedFnName} (${pos.source}:${pos.line}:${pos.column})`
327
324
  );
328
325
  }
329
326
  }
File without changes
@@ -6,10 +6,10 @@ import { fetchResult } from "../cfetch";
6
6
  import { getConfigCache, saveToConfigCache } from "../config-cache";
7
7
  import { confirm } from "../dialogs";
8
8
  import { getEnvironmentVariableFactory } from "../environment-variables";
9
+ import { CI } from "../is-ci";
9
10
  import isInteractive from "../is-interactive";
10
11
  import { logger } from "../logger";
11
12
  import { getAPIToken } from "../user";
12
- import { CI } from "./is-ci";
13
13
 
14
14
  /**
15
15
  * The date that the metrics being gathered was last updated in a way that would require
@@ -335,6 +335,11 @@ async function generateAssetsFetch(
335
335
 
336
336
  const generateResponse = (request: MiniflareRequest) => {
337
337
  const url = new URL(request.url);
338
+ let assetName = url.pathname;
339
+ try {
340
+ //it's possible for someone to send a URL like http://fakehost/abc%2 which would fail to decode
341
+ assetName = decodeURIComponent(url.pathname);
342
+ } catch {}
338
343
 
339
344
  const deconstructedResponse: {
340
345
  status: number;
@@ -377,7 +382,7 @@ async function generateAssetsFetch(
377
382
  }
378
383
 
379
384
  const notFound = () => {
380
- let cwd = url.pathname;
385
+ let cwd = assetName;
381
386
  while (cwd) {
382
387
  cwd = cwd.slice(0, cwd.lastIndexOf("/"));
383
388
 
@@ -407,38 +412,36 @@ async function generateAssetsFetch(
407
412
 
408
413
  let asset;
409
414
 
410
- if (url.pathname.endsWith("/")) {
411
- if ((asset = getAsset(`${url.pathname}/index.html`))) {
415
+ if (assetName.endsWith("/")) {
416
+ if ((asset = getAsset(`${assetName}/index.html`))) {
412
417
  deconstructedResponse.body = serveAsset(asset);
413
418
  deconstructedResponse.headers.set(
414
419
  "Content-Type",
415
420
  getType(asset) || "application/octet-stream"
416
421
  );
417
422
  return deconstructedResponse;
418
- } else if (
419
- (asset = getAsset(`${url.pathname.replace(/\/$/, ".html")}`))
420
- ) {
423
+ } else if ((asset = getAsset(`${assetName.replace(/\/$/, ".html")}`))) {
421
424
  deconstructedResponse.status = 301;
422
425
  deconstructedResponse.headers.set(
423
426
  "Location",
424
- `${url.pathname.slice(0, -1)}${url.search}`
427
+ `${assetName.slice(0, -1)}${url.search}`
425
428
  );
426
429
  return deconstructedResponse;
427
430
  }
428
431
  }
429
432
 
430
- if (url.pathname.endsWith("/index")) {
433
+ if (assetName.endsWith("/index")) {
431
434
  deconstructedResponse.status = 301;
432
435
  deconstructedResponse.headers.set(
433
436
  "Location",
434
- `${url.pathname.slice(0, -"index".length)}${url.search}`
437
+ `${assetName.slice(0, -"index".length)}${url.search}`
435
438
  );
436
439
  return deconstructedResponse;
437
440
  }
438
441
 
439
- if ((asset = getAsset(url.pathname))) {
440
- if (url.pathname.endsWith(".html")) {
441
- const extensionlessPath = url.pathname.slice(0, -".html".length);
442
+ if ((asset = getAsset(assetName))) {
443
+ if (assetName.endsWith(".html")) {
444
+ const extensionlessPath = assetName.slice(0, -".html".length);
442
445
  if (getAsset(extensionlessPath) || extensionlessPath === "/") {
443
446
  deconstructedResponse.body = serveAsset(asset);
444
447
  deconstructedResponse.headers.set(
@@ -462,12 +465,20 @@ async function generateAssetsFetch(
462
465
  );
463
466
  return deconstructedResponse;
464
467
  }
465
- } else if (hasFileExtension(url.pathname)) {
468
+ } else if (hasFileExtension(assetName)) {
469
+ if ((asset = getAsset(assetName + ".html"))) {
470
+ deconstructedResponse.body = serveAsset(asset);
471
+ deconstructedResponse.headers.set(
472
+ "Content-Type",
473
+ getType(asset) || "application/octet-stream"
474
+ );
475
+ return deconstructedResponse;
476
+ }
466
477
  notFound();
467
478
  return deconstructedResponse;
468
479
  }
469
480
 
470
- if ((asset = getAsset(`${url.pathname}.html`))) {
481
+ if ((asset = getAsset(`${assetName}.html`))) {
471
482
  deconstructedResponse.body = serveAsset(asset);
472
483
  deconstructedResponse.headers.set(
473
484
  "Content-Type",
@@ -476,11 +487,11 @@ async function generateAssetsFetch(
476
487
  return deconstructedResponse;
477
488
  }
478
489
 
479
- if ((asset = getAsset(`${url.pathname}/index.html`))) {
490
+ if ((asset = getAsset(`${assetName}/index.html`))) {
480
491
  deconstructedResponse.status = 301;
481
492
  deconstructedResponse.headers.set(
482
493
  "Location",
483
- `${url.pathname}/${url.search}`
494
+ `${assetName}/${url.search}`
484
495
  );
485
496
  return deconstructedResponse;
486
497
  } else {
@@ -1,10 +1,23 @@
1
- import { Log, LogLevel, Miniflare } from "miniflare";
1
+ import { fetch } from "@miniflare/core";
2
+ import {
3
+ DurableObjectNamespace,
4
+ DurableObjectStub,
5
+ } from "@miniflare/durable-objects";
6
+ import {
7
+ Log,
8
+ LogLevel,
9
+ Miniflare,
10
+ Response as MiniflareResponse,
11
+ Request as MiniflareRequest,
12
+ } from "miniflare";
2
13
  import yargs from "yargs";
3
14
  import { hideBin } from "yargs/helpers";
15
+ import { FatalError } from "../errors";
4
16
  import generateASSETSBinding from "./assets";
5
17
  import { enumKeys } from "./enum-keys";
6
18
  import { getRequestContextCheckOptions } from "./request-context";
7
19
  import type { Options } from "./assets";
20
+ import type { AddressInfo } from "net";
8
21
 
9
22
  export interface EnablePagesAssetsServiceBindingOptions {
10
23
  proxyPort?: number;
@@ -44,7 +57,48 @@ async function main() {
44
57
  console.log("OPTIONS:\n", JSON.stringify(config, null, 2));
45
58
  }
46
59
 
60
+ config.bindings = {
61
+ ...config.bindings,
62
+ ...Object.fromEntries(
63
+ Object.entries(
64
+ config.externalDurableObjects as Record<
65
+ string,
66
+ { name: string; host: string; port: number }
67
+ >
68
+ ).map(([binding, { name, host, port }]) => {
69
+ const factory = () => {
70
+ throw new FatalError(
71
+ "An external Durable Object instance's state has somehow been attempted to be accessed.",
72
+ 1
73
+ );
74
+ };
75
+ const namespace = new DurableObjectNamespace(name as string, factory);
76
+ namespace.get = (id) => {
77
+ const stub = new DurableObjectStub(factory, id);
78
+ stub.fetch = (...reqArgs) => {
79
+ const requestFromArgs = new MiniflareRequest(...reqArgs);
80
+ const url = new URL(requestFromArgs.url);
81
+ url.host = host;
82
+ if (port !== undefined) url.port = port.toString();
83
+ const request = new MiniflareRequest(
84
+ url.toString(),
85
+ requestFromArgs
86
+ );
87
+ request.headers.set("x-miniflare-durable-object-name", name);
88
+ request.headers.set("x-miniflare-durable-object-id", id.toString());
89
+
90
+ return fetch(request);
91
+ };
92
+ return stub;
93
+ };
94
+ return [binding, namespace];
95
+ })
96
+ ),
97
+ };
98
+
47
99
  let mf: Miniflare | undefined;
100
+ let durableObjectsMf: Miniflare | undefined = undefined;
101
+ let durableObjectsMfPort: number | undefined = undefined;
48
102
 
49
103
  try {
50
104
  if (args._[1]) {
@@ -73,12 +127,80 @@ async function main() {
73
127
  // Start Miniflare development server
74
128
  await mf.startServer();
75
129
  await mf.startScheduler();
76
- process.send && process.send("ready");
130
+
131
+ const internalDurableObjectClassNames = Object.values(
132
+ config.durableObjects as Record<string, string>
133
+ );
134
+
135
+ if (internalDurableObjectClassNames.length > 0) {
136
+ durableObjectsMf = new Miniflare({
137
+ host: config.host,
138
+ port: 0,
139
+ script: `
140
+ export default {
141
+ fetch(request, env) {
142
+ return env.DO.fetch(request)
143
+ }
144
+ }`,
145
+ serviceBindings: {
146
+ DO: async (request: MiniflareRequest) => {
147
+ request = new MiniflareRequest(request);
148
+
149
+ const name = request.headers.get("x-miniflare-durable-object-name");
150
+ const idString = request.headers.get(
151
+ "x-miniflare-durable-object-id"
152
+ );
153
+ request.headers.delete("x-miniflare-durable-object-name");
154
+ request.headers.delete("x-miniflare-durable-object-id");
155
+
156
+ if (!name || !idString) {
157
+ return new MiniflareResponse(
158
+ "[durable-object-proxy-err] Missing `x-miniflare-durable-object-name` or `x-miniflare-durable-object-id` headers.",
159
+ { status: 400 }
160
+ );
161
+ }
162
+
163
+ const namespace = await mf?.getDurableObjectNamespace(name);
164
+ const id = namespace?.idFromString(idString);
165
+
166
+ if (!id) {
167
+ return new MiniflareResponse(
168
+ "[durable-object-proxy-err] Could not generate an ID. Possibly due to a mismatched DO name and ID?",
169
+ { status: 500 }
170
+ );
171
+ }
172
+
173
+ const stub = namespace?.get(id);
174
+
175
+ if (!stub) {
176
+ return new MiniflareResponse(
177
+ "[durable-object-proxy-err] Could not generate a stub. Possibly due to a mismatched DO name and ID?",
178
+ { status: 500 }
179
+ );
180
+ }
181
+
182
+ return stub.fetch(request);
183
+ },
184
+ },
185
+ modules: true,
186
+ });
187
+ const server = await durableObjectsMf.startServer();
188
+ durableObjectsMfPort = (server.address() as AddressInfo).port;
189
+ }
190
+
191
+ process.send &&
192
+ process.send(
193
+ JSON.stringify({
194
+ ready: true,
195
+ durableObjectsPort: durableObjectsMfPort,
196
+ })
197
+ );
77
198
  } catch (e) {
78
199
  mf?.log.error(e as Error);
79
200
  process.exitCode = 1;
80
201
  // Unmount any mounted workers
81
202
  await mf?.dispose();
203
+ await durableObjectsMf?.dispose();
82
204
  }
83
205
  }
84
206
 
@@ -1,32 +1,29 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
+ import { FatalError } from "../errors";
4
5
  import { logger } from "../logger";
5
6
  import * as metrics from "../metrics";
6
7
  import { toUrlPath } from "../paths";
7
8
  import { isInPagesCI } from "./constants";
9
+ import {
10
+ EXIT_CODE_FUNCTIONS_NO_ROUTES_ERROR,
11
+ FunctionsNoRoutesError,
12
+ getFunctionsNoRoutesWarning,
13
+ } from "./errors";
8
14
  import { buildPlugin } from "./functions/buildPlugin";
9
15
  import { buildWorker } from "./functions/buildWorker";
10
16
  import { generateConfigFromFileTree } from "./functions/filepath-routing";
11
17
  import { writeRoutesModule } from "./functions/routes";
18
+ import { convertRoutesToRoutesJSONSpec } from "./functions/routes-transformation";
12
19
  import { pagesBetaWarning, RUNNING_BUILDERS } from "./utils";
13
20
  import type { Config } from "./functions/routes";
14
- import type { ArgumentsCamelCase, Argv } from "yargs";
21
+ import type { YargsOptionsToInterface } from "./types";
22
+ import type { Argv } from "yargs";
15
23
 
16
- type PagesBuildArgs = {
17
- directory: string;
18
- outfile: string;
19
- "output-config-path"?: string;
20
- minify: boolean;
21
- sourcemap: boolean;
22
- "fallback-service": string;
23
- watch: boolean;
24
- plugin: boolean;
25
- "build-output-directory"?: string;
26
- "node-compat": boolean;
27
- };
24
+ type PagesBuildArgs = YargsOptionsToInterface<typeof Options>;
28
25
 
29
- export function Options(yargs: Argv): Argv<PagesBuildArgs> {
26
+ export function Options(yargs: Argv) {
30
27
  return yargs
31
28
  .positional("directory", {
32
29
  type: "string",
@@ -43,6 +40,10 @@ export function Options(yargs: Argv): Argv<PagesBuildArgs> {
43
40
  type: "string",
44
41
  description: "The location for the output config file",
45
42
  },
43
+ "output-routes-path": {
44
+ type: "string",
45
+ description: "The location for the output _routes.json file",
46
+ },
46
47
  minify: {
47
48
  type: "boolean",
48
49
  default: false,
@@ -87,15 +88,16 @@ export function Options(yargs: Argv): Argv<PagesBuildArgs> {
87
88
  export const Handler = async ({
88
89
  directory,
89
90
  outfile,
90
- "output-config-path": outputConfigPath,
91
+ outputConfigPath,
92
+ outputRoutesPath: routesOutputPath,
91
93
  minify,
92
94
  sourcemap,
93
95
  fallbackService,
94
96
  watch,
95
97
  plugin,
96
- "build-output-directory": buildOutputDirectory,
97
- "node-compat": nodeCompat,
98
- }: ArgumentsCamelCase<PagesBuildArgs>) => {
98
+ buildOutputDirectory,
99
+ nodeCompat,
100
+ }: PagesBuildArgs) => {
99
101
  if (!isInPagesCI) {
100
102
  // Beta message for `wrangler pages <commands>` usage
101
103
  logger.log(pagesBetaWarning);
@@ -108,22 +110,37 @@ export const Handler = async ({
108
110
  }
109
111
 
110
112
  buildOutputDirectory ??= dirname(outfile);
111
-
112
- await buildFunctions({
113
- outfile,
114
- outputConfigPath,
115
- functionsDirectory: directory,
116
- minify,
117
- sourcemap,
118
- fallbackService,
119
- watch,
120
- plugin,
121
- buildOutputDirectory,
122
- nodeCompat,
123
- });
113
+ try {
114
+ await buildFunctions({
115
+ outfile,
116
+ outputConfigPath,
117
+ functionsDirectory: directory,
118
+ minify,
119
+ sourcemap,
120
+ fallbackService,
121
+ watch,
122
+ plugin,
123
+ buildOutputDirectory,
124
+ nodeCompat,
125
+ routesOutputPath,
126
+ });
127
+ } catch (e) {
128
+ if (e instanceof FunctionsNoRoutesError) {
129
+ throw new FatalError(
130
+ getFunctionsNoRoutesWarning(directory),
131
+ EXIT_CODE_FUNCTIONS_NO_ROUTES_ERROR
132
+ );
133
+ } else {
134
+ throw e;
135
+ }
136
+ }
124
137
  await metrics.sendMetricsEvent("build pages functions");
125
138
  };
126
139
 
140
+ /**
141
+ * Builds a Functions worker based on the functions directory, with filepath and handler based routing.
142
+ * @throws FunctionsNoRoutesError when there are no routes found in the functions directory
143
+ */
127
144
  export async function buildFunctions({
128
145
  outfile,
129
146
  outputConfigPath,
@@ -135,19 +152,25 @@ export async function buildFunctions({
135
152
  onEnd,
136
153
  plugin = false,
137
154
  buildOutputDirectory,
155
+ routesOutputPath,
138
156
  nodeCompat,
139
- }: {
140
- outfile: string;
141
- outputConfigPath?: string;
157
+ }: Partial<
158
+ Pick<
159
+ PagesBuildArgs,
160
+ | "outputConfigPath"
161
+ | "minify"
162
+ | "sourcemap"
163
+ | "fallbackService"
164
+ | "watch"
165
+ | "plugin"
166
+ | "buildOutputDirectory"
167
+ | "nodeCompat"
168
+ >
169
+ > & {
142
170
  functionsDirectory: string;
143
- minify?: boolean;
144
- sourcemap?: boolean;
145
- fallbackService?: string;
146
- watch?: boolean;
147
171
  onEnd?: () => void;
148
- plugin?: boolean;
149
- buildOutputDirectory?: string;
150
- nodeCompat?: boolean;
172
+ outfile: Required<PagesBuildArgs>["outfile"];
173
+ routesOutputPath?: PagesBuildArgs["outputRoutesPath"];
151
174
  }) {
152
175
  RUNNING_BUILDERS.forEach(
153
176
  (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
@@ -161,6 +184,17 @@ export async function buildFunctions({
161
184
  baseURL,
162
185
  });
163
186
 
187
+ if (!config.routes || config.routes.length === 0) {
188
+ throw new FunctionsNoRoutesError(
189
+ `Failed to find any routes while compiling Functions in: ${functionsDirectory}`
190
+ );
191
+ }
192
+
193
+ if (routesOutputPath) {
194
+ const routesJSON = convertRoutesToRoutesJSONSpec(config.routes);
195
+ writeFileSync(routesOutputPath, JSON.stringify(routesJSON, null, 2));
196
+ }
197
+
164
198
  if (outputConfigPath) {
165
199
  writeFileSync(
166
200
  outputConfigPath,