wrangler 2.0.5 → 2.0.8

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/README.md +1 -1
  2. package/bin/wrangler.js +16 -4
  3. package/package.json +6 -4
  4. package/pages/functions/buildPlugin.ts +13 -0
  5. package/pages/functions/buildWorker.ts +13 -0
  6. package/src/__tests__/configuration.test.ts +335 -86
  7. package/src/__tests__/dev.test.tsx +166 -15
  8. package/src/__tests__/helpers/mock-dialogs.ts +41 -1
  9. package/src/__tests__/index.test.ts +30 -16
  10. package/src/__tests__/init.test.ts +249 -131
  11. package/src/__tests__/kv.test.ts +101 -101
  12. package/src/__tests__/package-manager.test.ts +154 -7
  13. package/src/__tests__/pages.test.ts +369 -39
  14. package/src/__tests__/parse.test.ts +5 -1
  15. package/src/__tests__/publish.test.ts +556 -84
  16. package/src/__tests__/r2.test.ts +47 -24
  17. package/src/__tests__/secret.test.ts +39 -4
  18. package/src/abort.d.ts +3 -0
  19. package/src/bundle.ts +32 -1
  20. package/src/cfetch/index.ts +21 -4
  21. package/src/cfetch/internal.ts +14 -9
  22. package/src/config/environment.ts +40 -14
  23. package/src/config/index.ts +162 -0
  24. package/src/config/validation.ts +179 -64
  25. package/src/create-worker-preview.ts +17 -7
  26. package/src/create-worker-upload-form.ts +22 -8
  27. package/src/dev/dev.tsx +2 -4
  28. package/src/dev/local.tsx +6 -0
  29. package/src/dev/remote.tsx +15 -1
  30. package/src/dialogs.tsx +48 -0
  31. package/src/durable.ts +102 -0
  32. package/src/index.tsx +314 -144
  33. package/src/inspect.ts +39 -0
  34. package/src/kv.ts +77 -13
  35. package/src/open-in-browser.ts +5 -12
  36. package/src/package-manager.ts +50 -3
  37. package/src/pages.tsx +210 -65
  38. package/src/parse.ts +21 -4
  39. package/src/proxy.ts +38 -22
  40. package/src/publish.ts +227 -113
  41. package/src/sites.tsx +11 -9
  42. package/src/worker.ts +8 -0
  43. package/templates/new-worker-scheduled.js +17 -0
  44. package/templates/new-worker-scheduled.ts +32 -0
  45. package/templates/new-worker.ts +16 -1
  46. package/wrangler-dist/cli.js +35466 -22362
package/src/pages.tsx CHANGED
@@ -4,7 +4,7 @@ import { execSync, spawn } from "node:child_process";
4
4
  import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { readdir, readFile, stat } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
- import { dirname, join, sep } from "node:path";
7
+ import { dirname, join, sep, extname, basename } from "node:path";
8
8
  import { cwd } from "node:process";
9
9
  import { URL } from "node:url";
10
10
  import { hash } from "blake3-wasm";
@@ -14,6 +14,7 @@ import SelectInput from "ink-select-input";
14
14
  import Spinner from "ink-spinner";
15
15
  import Table from "ink-table";
16
16
  import { getType } from "mime";
17
+ import PQueue from "p-queue";
17
18
  import prettyBytes from "pretty-bytes";
18
19
  import React from "react";
19
20
  import { format as timeagoFormat } from "timeago.js";
@@ -32,7 +33,11 @@ import openInBrowser from "./open-in-browser";
32
33
  import { toUrlPath } from "./paths";
33
34
  import { requireAuth } from "./user";
34
35
  import type { Config } from "../pages/functions/routes";
35
- import type { Headers, Request, fetch } from "@miniflare/core";
36
+ import type {
37
+ Headers as MiniflareHeaders,
38
+ Request as MiniflareRequest,
39
+ fetch as miniflareFetch,
40
+ } from "@miniflare/core";
36
41
  import type { BuildResult } from "esbuild";
37
42
  import type { MiniflareOptions } from "miniflare";
38
43
  import type { BuilderCallback, CommandModule } from "yargs";
@@ -68,12 +73,23 @@ export type Deployment = {
68
73
  project_name: string;
69
74
  };
70
75
 
76
+ export type UploadPayloadFile = {
77
+ key: string;
78
+ value: string;
79
+ metadata: { contentType: string };
80
+ base64: boolean;
81
+ };
82
+
71
83
  interface PagesConfigCache {
72
84
  account_id?: string;
73
85
  project_name?: string;
74
86
  }
75
87
 
76
88
  const PAGES_CONFIG_CACHE_FILENAME = "pages.json";
89
+ const MAX_BUCKET_SIZE = 50 * 1024 * 1024;
90
+ const MAX_BUCKET_FILE_COUNT = 5000;
91
+ const BULK_UPLOAD_CONCURRENCY = 3;
92
+ const MAX_UPLOAD_ATTEMPTS = 5;
77
93
 
78
94
  // Defer importing miniflare until we really need it. This takes ~0.5s
79
95
  // and also modifies some `stream/web` and `undici` prototypes, so we
@@ -284,7 +300,7 @@ function generateRulesMatcher<T>(
284
300
  T
285
301
  ][];
286
302
 
287
- return ({ request }: { request: Request }) => {
303
+ return ({ request }: { request: MiniflareRequest }) => {
288
304
  const { pathname, host } = new URL(request.url);
289
305
 
290
306
  return compiledRules
@@ -357,7 +373,7 @@ function generateHeadersMatcher(headersFile: string) {
357
373
  )
358
374
  );
359
375
 
360
- return (request: Request) => {
376
+ return (request: MiniflareRequest) => {
361
377
  const matches = rulesMatcher({
362
378
  request,
363
379
  });
@@ -406,7 +422,7 @@ function generateRedirectsMatcher(redirectsFile: string) {
406
422
  })
407
423
  );
408
424
 
409
- return (request: Request) => {
425
+ return (request: MiniflareRequest) => {
410
426
  const match = rulesMatcher({
411
427
  request,
412
428
  })[0];
@@ -461,7 +477,9 @@ function hasFileExtension(pathname: string) {
461
477
  return /\/.+\.[a-z0-9]+$/i.test(pathname);
462
478
  }
463
479
 
464
- async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
480
+ async function generateAssetsFetch(
481
+ directory: string
482
+ ): Promise<typeof miniflareFetch> {
465
483
  // Defer importing miniflare until we really need it
466
484
  const { Headers, Request, Response } = await import("@miniflare/core");
467
485
 
@@ -510,12 +528,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
510
528
  return readFileSync(file);
511
529
  };
512
530
 
513
- const generateResponse = (request: Request) => {
531
+ const generateResponse = (request: MiniflareRequest) => {
514
532
  const url = new URL(request.url);
515
533
 
516
534
  const deconstructedResponse: {
517
535
  status: number;
518
- headers: Headers;
536
+ headers: MiniflareHeaders;
519
537
  body?: Buffer;
520
538
  } = {
521
539
  status: 200,
@@ -667,8 +685,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
667
685
  };
668
686
 
669
687
  const attachHeaders = (
670
- request: Request,
671
- deconstructedResponse: { status: number; headers: Headers; body?: Buffer }
688
+ request: MiniflareRequest,
689
+ deconstructedResponse: {
690
+ status: number;
691
+ headers: MiniflareHeaders;
692
+ body?: Buffer;
693
+ }
672
694
  ) => {
673
695
  const headers = deconstructedResponse.headers;
674
696
  const newHeaders = new Headers({});
@@ -722,6 +744,7 @@ async function buildFunctions({
722
744
  onEnd,
723
745
  plugin = false,
724
746
  buildOutputDirectory,
747
+ nodeCompat,
725
748
  }: {
726
749
  outfile: string;
727
750
  outputConfigPath?: string;
@@ -733,12 +756,13 @@ async function buildFunctions({
733
756
  onEnd?: () => void;
734
757
  plugin?: boolean;
735
758
  buildOutputDirectory?: string;
759
+ nodeCompat?: boolean;
736
760
  }) {
737
761
  RUNNING_BUILDERS.forEach(
738
762
  (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
739
763
  );
740
764
 
741
- const routesModule = join(tmpdir(), "./functionsRoutes.mjs");
765
+ const routesModule = join(tmpdir(), `./functionsRoutes-${Math.random()}.mjs`);
742
766
  const baseURL = toUrlPath("/");
743
767
 
744
768
  const config: Config = await generateConfigFromFileTree({
@@ -767,6 +791,7 @@ async function buildFunctions({
767
791
  minify,
768
792
  sourcemap,
769
793
  watch,
794
+ nodeCompat,
770
795
  onEnd,
771
796
  })
772
797
  );
@@ -781,6 +806,7 @@ async function buildFunctions({
781
806
  watch,
782
807
  onEnd,
783
808
  buildOutputDirectory,
809
+ nodeCompat,
784
810
  })
785
811
  );
786
812
  }
@@ -1001,7 +1027,7 @@ const createDeployment: CommandModule<
1001
1027
 
1002
1028
  if (isGitDirty && !commitDirty) {
1003
1029
  logger.warn(
1004
- `Warning: Your working directory is a git repo and has uncommitted changes\nTo silense this warning, pass in --commit-dirty=true`
1030
+ `Warning: Your working directory is a git repo and has uncommitted changes\nTo silence this warning, pass in --commit-dirty=true`
1005
1031
  );
1006
1032
  }
1007
1033
 
@@ -1013,7 +1039,7 @@ const createDeployment: CommandModule<
1013
1039
  let builtFunctions: string | undefined = undefined;
1014
1040
  const functionsDirectory = join(cwd(), "functions");
1015
1041
  if (existsSync(functionsDirectory)) {
1016
- const outfile = join(tmpdir(), "./functionsWorker.js");
1042
+ const outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`);
1017
1043
 
1018
1044
  await new Promise((resolve) =>
1019
1045
  buildFunctions({
@@ -1027,12 +1053,9 @@ const createDeployment: CommandModule<
1027
1053
  builtFunctions = readFileSync(outfile, "utf-8");
1028
1054
  }
1029
1055
 
1030
- type File = {
1031
- content: Buffer;
1032
- metadata: Metadata;
1033
- };
1034
-
1035
- type Metadata = {
1056
+ type FileContainer = {
1057
+ content: string;
1058
+ contentType: string;
1036
1059
  sizeInBytes: number;
1037
1060
  hash: string;
1038
1061
  };
@@ -1043,11 +1066,12 @@ const createDeployment: CommandModule<
1043
1066
  "_headers",
1044
1067
  ".DS_Store",
1045
1068
  "node_modules",
1069
+ ".git",
1046
1070
  ];
1047
1071
 
1048
1072
  const walk = async (
1049
1073
  dir: string,
1050
- fileMap: Map<string, File> = new Map(),
1074
+ fileMap: Map<string, FileContainer> = new Map(),
1051
1075
  depth = 0
1052
1076
  ) => {
1053
1077
  const files = await readdir(dir);
@@ -1079,10 +1103,7 @@ const createDeployment: CommandModule<
1079
1103
  const fileContent = await readFile(filepath);
1080
1104
 
1081
1105
  const base64Content = fileContent.toString("base64");
1082
- const extension =
1083
- name.split(".").length > 1 ? name.split(".").at(-1) || "" : "";
1084
-
1085
- const content = base64Content + extension;
1106
+ const extension = extname(basename(name)).substring(1);
1086
1107
 
1087
1108
  if (filestat.size > 25 * 1024 * 1024) {
1088
1109
  throw new Error(
@@ -1093,11 +1114,12 @@ const createDeployment: CommandModule<
1093
1114
  }
1094
1115
 
1095
1116
  fileMap.set(name, {
1096
- content: fileContent,
1097
- metadata: {
1098
- sizeInBytes: filestat.size,
1099
- hash: hash(content).toString("hex").slice(0, 32),
1100
- },
1117
+ content: base64Content,
1118
+ contentType: getType(name) || "application/octet-stream",
1119
+ sizeInBytes: filestat.size,
1120
+ hash: hash(base64Content + extension)
1121
+ .toString("hex")
1122
+ .slice(0, 32),
1101
1123
  });
1102
1124
  }
1103
1125
  })
@@ -1108,51 +1130,139 @@ const createDeployment: CommandModule<
1108
1130
 
1109
1131
  const fileMap = await walk(directory);
1110
1132
 
1133
+ if (fileMap.size > 20000) {
1134
+ throw new FatalError(
1135
+ `Error: Pages only supports up to 20,000 files in a deployment. Ensure you have specified your build output directory correctly.`,
1136
+ 1
1137
+ );
1138
+ }
1139
+
1140
+ const files = [...fileMap.values()];
1141
+
1142
+ const { jwt } = await fetchResult<{ jwt: string }>(
1143
+ `/accounts/${accountId}/pages/projects/${projectName}/upload-token`
1144
+ );
1145
+
1111
1146
  const start = Date.now();
1112
1147
 
1113
- const files: Array<Promise<void>> = [];
1148
+ const missingHashes = await fetchResult<string[]>(
1149
+ `/pages/assets/check-missing`,
1150
+ {
1151
+ method: "POST",
1152
+ headers: {
1153
+ "Content-Type": "application/json",
1154
+ Authorization: `Bearer ${jwt}`,
1155
+ },
1156
+ body: JSON.stringify({
1157
+ hashes: files.map(({ hash }) => hash),
1158
+ }),
1159
+ }
1160
+ );
1114
1161
 
1115
- if (fileMap.size > 1000) {
1116
- throw new Error(
1117
- `Error: Pages only supports up to 1,000 files in a deployment at the moment.\nTry a smaller project perhaps?`
1118
- );
1119
- }
1162
+ const sortedFiles = files
1163
+ .filter((file) => missingHashes.includes(file.hash))
1164
+ .sort((a, b) => b.sizeInBytes - a.sizeInBytes);
1165
+
1166
+ // Start with a few buckets so small projects still get
1167
+ // the benefit of multiple upload streams
1168
+ const buckets: {
1169
+ files: FileContainer[];
1170
+ remainingSize: number;
1171
+ }[] = new Array(BULK_UPLOAD_CONCURRENCY).fill(null).map(() => ({
1172
+ files: [],
1173
+ remainingSize: MAX_BUCKET_SIZE,
1174
+ }));
1175
+
1176
+ let bucketOffset = 0;
1177
+ for (const file of sortedFiles) {
1178
+ let inserted = false;
1179
+
1180
+ for (let i = 0; i < buckets.length; i++) {
1181
+ // Start at a different bucket for each new file
1182
+ const bucket = buckets[(i + bucketOffset) % buckets.length];
1183
+ if (
1184
+ bucket.remainingSize >= file.sizeInBytes &&
1185
+ bucket.files.length < MAX_BUCKET_FILE_COUNT
1186
+ ) {
1187
+ bucket.files.push(file);
1188
+ bucket.remainingSize -= file.sizeInBytes;
1189
+ inserted = true;
1190
+ break;
1191
+ }
1192
+ }
1120
1193
 
1121
- let counter = 0;
1194
+ if (!inserted) {
1195
+ buckets.push({
1196
+ files: [file],
1197
+ remainingSize: MAX_BUCKET_SIZE - file.sizeInBytes,
1198
+ });
1199
+ }
1200
+ bucketOffset++;
1201
+ }
1122
1202
 
1203
+ let counter = fileMap.size - sortedFiles.length;
1123
1204
  const { rerender, unmount } = render(
1124
1205
  <Progress done={counter} total={fileMap.size} />
1125
1206
  );
1126
1207
 
1127
- fileMap.forEach((file: File, name: string) => {
1128
- const form = new FormData();
1129
- form.append(
1130
- "file",
1131
- new File([new Uint8Array(file.content.buffer)], name)
1132
- );
1208
+ const queue = new PQueue({ concurrency: BULK_UPLOAD_CONCURRENCY });
1133
1209
 
1134
- // TODO: Consider a retry
1210
+ for (const bucket of buckets) {
1211
+ // Don't upload empty buckets (can happen for tiny projects)
1212
+ if (bucket.files.length === 0) continue;
1135
1213
 
1136
- const promise = fetchResult<{ id: string }>(
1137
- `/accounts/${accountId}/pages/projects/${projectName}/file`,
1138
- {
1139
- method: "POST",
1140
- body: form,
1141
- }
1142
- ).then((response) => {
1143
- counter++;
1144
- rerender(<Progress done={counter} total={fileMap.size} />);
1145
- if (response.id != file.metadata.hash) {
1146
- throw new Error(
1147
- `Looks like there was an issue uploading '${name}'. Try again perhaps?`
1148
- );
1214
+ const payload: UploadPayloadFile[] = bucket.files.map((file) => ({
1215
+ key: file.hash,
1216
+ value: file.content,
1217
+ metadata: {
1218
+ contentType: file.contentType,
1219
+ },
1220
+ base64: true,
1221
+ }));
1222
+
1223
+ let attempts = 0;
1224
+ const doUpload = async (): Promise<void> => {
1225
+ try {
1226
+ return await fetchResult(`/pages/assets/upload`, {
1227
+ method: "POST",
1228
+ headers: {
1229
+ "Content-Type": "application/json",
1230
+ Authorization: `Bearer ${jwt}`,
1231
+ },
1232
+ body: JSON.stringify(payload),
1233
+ });
1234
+ } catch (e) {
1235
+ if (attempts < MAX_UPLOAD_ATTEMPTS) {
1236
+ // Linear backoff, 0 second first time, then 1 second etc.
1237
+ await new Promise((resolve) =>
1238
+ setTimeout(resolve, attempts++ * 1000)
1239
+ );
1240
+ return doUpload();
1241
+ } else {
1242
+ throw e;
1243
+ }
1149
1244
  }
1150
- });
1245
+ };
1151
1246
 
1152
- files.push(promise);
1153
- });
1247
+ queue.add(() =>
1248
+ doUpload().then(
1249
+ () => {
1250
+ counter += bucket.files.length;
1251
+ rerender(<Progress done={counter} total={fileMap.size} />);
1252
+ },
1253
+ (error) => {
1254
+ return Promise.reject(
1255
+ new FatalError(
1256
+ "Failed to upload files. Please try again.",
1257
+ error.code || 1
1258
+ )
1259
+ );
1260
+ }
1261
+ )
1262
+ );
1263
+ }
1154
1264
 
1155
- await Promise.all(files);
1265
+ await queue.onIdle();
1156
1266
 
1157
1267
  unmount();
1158
1268
 
@@ -1170,7 +1280,7 @@ const createDeployment: CommandModule<
1170
1280
  Object.fromEntries(
1171
1281
  [...fileMap.entries()].map(([fileName, file]) => [
1172
1282
  `/${fileName}`,
1173
- file.metadata.hash,
1283
+ file.hash,
1174
1284
  ])
1175
1285
  )
1176
1286
  )
@@ -1244,7 +1354,7 @@ const createDeployment: CommandModule<
1244
1354
  export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1245
1355
  return yargs
1246
1356
  .command(
1247
- "dev [directory] [-- command]",
1357
+ "dev [directory] [-- command..]",
1248
1358
  "🧑‍💻 Develop your full-stack Pages application locally",
1249
1359
  (yargs) => {
1250
1360
  return yargs
@@ -1300,6 +1410,12 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1300
1410
  default: false,
1301
1411
  description: "Auto reload HTML pages when change is detected",
1302
1412
  },
1413
+ "node-compat": {
1414
+ describe: "Enable node.js compatibility",
1415
+ default: false,
1416
+ type: "boolean",
1417
+ hidden: true,
1418
+ },
1303
1419
  // TODO: Miniflare user options
1304
1420
  })
1305
1421
  .epilogue(pagesBetaWarning);
@@ -1314,6 +1430,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1314
1430
  kv: kvs = [],
1315
1431
  do: durableObjects = [],
1316
1432
  "live-reload": liveReload,
1433
+ "node-compat": nodeCompat,
1317
1434
  _: [_pages, _dev, ...remaining],
1318
1435
  }) => {
1319
1436
  // Beta message for `wrangler pages <commands>` usage
@@ -1347,7 +1464,16 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1347
1464
  );
1348
1465
 
1349
1466
  if (usingFunctions) {
1350
- const outfile = join(tmpdir(), "./functionsWorker.js");
1467
+ const outfile = join(
1468
+ tmpdir(),
1469
+ `./functionsWorker-${Math.random()}.js`
1470
+ );
1471
+
1472
+ if (nodeCompat) {
1473
+ console.warn(
1474
+ "Enabling node.js compatibility mode for builtins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
1475
+ );
1476
+ }
1351
1477
 
1352
1478
  logger.log(`Compiling worker to "${outfile}"...`);
1353
1479
 
@@ -1359,6 +1485,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1359
1485
  watch: true,
1360
1486
  onEnd: () => scriptReadyResolve(),
1361
1487
  buildOutputDirectory: directory,
1488
+ nodeCompat,
1362
1489
  });
1363
1490
  } catch {}
1364
1491
 
@@ -1373,6 +1500,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1373
1500
  watch: true,
1374
1501
  onEnd: () => scriptReadyResolve(),
1375
1502
  buildOutputDirectory: directory,
1503
+ nodeCompat,
1376
1504
  });
1377
1505
  });
1378
1506
 
@@ -1395,6 +1523,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1395
1523
  } else {
1396
1524
  logger.log("No functions. Shimming...");
1397
1525
  miniflareArgs = {
1526
+ // cfFetch sets the `cf` object that a function could expect
1527
+ // If there are no functions, there's no reason to set this up (and not make that network call)
1528
+ cfFetch: false,
1398
1529
  // TODO: The fact that these request/response hacks are necessary is ridiculous.
1399
1530
  // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
1400
1531
  script: `
@@ -1456,7 +1587,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1456
1587
 
1457
1588
  // env.ASSETS.fetch
1458
1589
  serviceBindings: {
1459
- async ASSETS(request: Request) {
1590
+ async ASSETS(request: MiniflareRequest) {
1460
1591
  if (proxyPort) {
1461
1592
  try {
1462
1593
  const url = new URL(request.url);
@@ -1580,6 +1711,12 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1580
1711
  type: "string",
1581
1712
  description: "The directory to output static assets to",
1582
1713
  },
1714
+ "node-compat": {
1715
+ describe: "Enable node.js compatibility",
1716
+ default: false,
1717
+ type: "boolean",
1718
+ hidden: true,
1719
+ },
1583
1720
  })
1584
1721
  .epilogue(pagesBetaWarning),
1585
1722
  async ({
@@ -1592,12 +1729,19 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1592
1729
  watch,
1593
1730
  plugin,
1594
1731
  "build-output-directory": buildOutputDirectory,
1732
+ "node-compat": nodeCompat,
1595
1733
  }) => {
1596
1734
  if (!isInPagesCI) {
1597
1735
  // Beta message for `wrangler pages <commands>` usage
1598
1736
  logger.log(pagesBetaWarning);
1599
1737
  }
1600
1738
 
1739
+ if (nodeCompat) {
1740
+ console.warn(
1741
+ "Enabling node.js compatibility mode for builtins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
1742
+ );
1743
+ }
1744
+
1601
1745
  buildOutputDirectory ??= dirname(outfile);
1602
1746
 
1603
1747
  await buildFunctions({
@@ -1610,6 +1754,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1610
1754
  watch,
1611
1755
  plugin,
1612
1756
  buildOutputDirectory,
1757
+ nodeCompat,
1613
1758
  });
1614
1759
  }
1615
1760
  )
@@ -1834,7 +1979,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1834
1979
  } as CommandModule);
1835
1980
  };
1836
1981
 
1837
- const invalidAssetsFetch: typeof fetch = () => {
1982
+ const invalidAssetsFetch: typeof miniflareFetch = () => {
1838
1983
  throw new Error(
1839
1984
  "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
1840
1985
  );
package/src/parse.ts CHANGED
@@ -62,8 +62,6 @@ export class ParseError extends Error implements Message {
62
62
  }
63
63
  }
64
64
 
65
- /* eslint-disable @typescript-eslint/no-explicit-any */
66
-
67
65
  const TOML_ERROR_NAME = "TomlError";
68
66
  const TOML_ERROR_SUFFIX = " at row ";
69
67
 
@@ -78,7 +76,7 @@ type TomlError = Error & {
78
76
  export function parseTOML(input: string, file?: string): TOML.JsonMap | never {
79
77
  try {
80
78
  // Normalize CRLF to LF to avoid hitting https://github.com/iarna/iarna-toml/issues/33.
81
- const normalizedInput = input.replace(/\r\n$/g, "\n");
79
+ const normalizedInput = input.replace(/\r\n/g, "\n");
82
80
  return TOML.parse(normalizedInput);
83
81
  } catch (err) {
84
82
  const { name, message, line, col } = err as TomlError;
@@ -100,10 +98,29 @@ export function parseTOML(input: string, file?: string): TOML.JsonMap | never {
100
98
 
101
99
  const JSON_ERROR_SUFFIX = " in JSON at position ";
102
100
 
101
+ /**
102
+ * A minimal type describing a package.json file.
103
+ */
104
+ export type PackageJSON = {
105
+ devDependencies?: Record<string, unknown>;
106
+ dependencies?: Record<string, unknown>;
107
+ scripts?: Record<string, unknown>;
108
+ };
109
+
110
+ /**
111
+ * A typed version of `parseJSON()`.
112
+ */
113
+ export function parsePackageJSON<T extends PackageJSON = PackageJSON>(
114
+ input: string,
115
+ file?: string
116
+ ): T {
117
+ return parseJSON<T>(input, file);
118
+ }
119
+
103
120
  /**
104
121
  * A wrapper around `JSON.parse` that throws a `ParseError`.
105
122
  */
106
- export function parseJSON(input: string, file?: string): any {
123
+ export function parseJSON<T>(input: string, file?: string): T {
107
124
  try {
108
125
  return JSON.parse(input);
109
126
  } catch (err) {