wrangler 2.0.6 → 2.0.9

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 +132 -60
  7. package/src/__tests__/dev.test.tsx +168 -67
  8. package/src/__tests__/helpers/mock-dialogs.ts +41 -1
  9. package/src/__tests__/index.test.ts +25 -10
  10. package/src/__tests__/init.test.ts +252 -131
  11. package/src/__tests__/kv.test.ts +16 -16
  12. package/src/__tests__/package-manager.test.ts +154 -7
  13. package/src/__tests__/pages.test.ts +442 -38
  14. package/src/__tests__/parse.test.ts +5 -1
  15. package/src/__tests__/publish.test.ts +377 -84
  16. package/src/__tests__/secret.test.ts +4 -4
  17. package/src/__tests__/whoami.test.tsx +34 -0
  18. package/src/abort.d.ts +3 -0
  19. package/src/cfetch/index.ts +21 -4
  20. package/src/cfetch/internal.ts +20 -18
  21. package/src/config/config.ts +1 -1
  22. package/src/config/index.ts +162 -0
  23. package/src/config/validation.ts +77 -29
  24. package/src/create-worker-preview.ts +32 -22
  25. package/src/dev/dev.tsx +6 -16
  26. package/src/dev/remote.tsx +40 -16
  27. package/src/dialogs.tsx +48 -0
  28. package/src/durable.ts +102 -0
  29. package/src/index.tsx +291 -207
  30. package/src/inspect.ts +39 -0
  31. package/src/kv.ts +74 -25
  32. package/src/open-in-browser.ts +5 -12
  33. package/src/package-manager.ts +50 -3
  34. package/src/pages.tsx +218 -61
  35. package/src/parse.ts +21 -4
  36. package/src/proxy.ts +38 -22
  37. package/src/publish.ts +166 -108
  38. package/src/sites.tsx +8 -8
  39. package/src/user.tsx +12 -1
  40. package/src/whoami.tsx +3 -2
  41. package/src/worker.ts +2 -1
  42. package/src/zones.ts +73 -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 +33066 -20052
package/src/pages.tsx CHANGED
@@ -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);
@@ -1081,8 +1105,6 @@ const createDeployment: CommandModule<
1081
1105
  const base64Content = fileContent.toString("base64");
1082
1106
  const extension = extname(basename(name)).substring(1);
1083
1107
 
1084
- const content = base64Content + extension;
1085
-
1086
1108
  if (filestat.size > 25 * 1024 * 1024) {
1087
1109
  throw new Error(
1088
1110
  `Error: Pages only supports files up to ${prettyBytes(
@@ -1092,11 +1114,12 @@ const createDeployment: CommandModule<
1092
1114
  }
1093
1115
 
1094
1116
  fileMap.set(name, {
1095
- content: fileContent,
1096
- metadata: {
1097
- sizeInBytes: filestat.size,
1098
- hash: hash(content).toString("hex").slice(0, 32),
1099
- },
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),
1100
1123
  });
1101
1124
  }
1102
1125
  })
@@ -1107,51 +1130,150 @@ const createDeployment: CommandModule<
1107
1130
 
1108
1131
  const fileMap = await walk(directory);
1109
1132
 
1110
- const start = Date.now();
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
+ }
1111
1139
 
1112
- const files: Array<Promise<void>> = [];
1140
+ const files = [...fileMap.values()];
1113
1141
 
1114
- if (fileMap.size > 1000) {
1115
- throw new Error(
1116
- `Error: Pages only supports up to 1,000 files in a deployment at the moment.\nTry a smaller project perhaps?`
1117
- );
1142
+ async function fetchJwt(): Promise<string> {
1143
+ return (
1144
+ await fetchResult<{ jwt: string }>(
1145
+ `/accounts/${accountId}/pages/projects/${projectName}/upload-token`
1146
+ )
1147
+ ).jwt;
1118
1148
  }
1119
1149
 
1120
- let counter = 0;
1150
+ let jwt = await fetchJwt();
1151
+
1152
+ const start = Date.now();
1121
1153
 
1154
+ const missingHashes = await fetchResult<string[]>(
1155
+ `/pages/assets/check-missing`,
1156
+ {
1157
+ method: "POST",
1158
+ headers: {
1159
+ "Content-Type": "application/json",
1160
+ Authorization: `Bearer ${jwt}`,
1161
+ },
1162
+ body: JSON.stringify({
1163
+ hashes: files.map(({ hash }) => hash),
1164
+ }),
1165
+ }
1166
+ );
1167
+
1168
+ const sortedFiles = files
1169
+ .filter((file) => missingHashes.includes(file.hash))
1170
+ .sort((a, b) => b.sizeInBytes - a.sizeInBytes);
1171
+
1172
+ // Start with a few buckets so small projects still get
1173
+ // the benefit of multiple upload streams
1174
+ const buckets: {
1175
+ files: FileContainer[];
1176
+ remainingSize: number;
1177
+ }[] = new Array(BULK_UPLOAD_CONCURRENCY).fill(null).map(() => ({
1178
+ files: [],
1179
+ remainingSize: MAX_BUCKET_SIZE,
1180
+ }));
1181
+
1182
+ let bucketOffset = 0;
1183
+ for (const file of sortedFiles) {
1184
+ let inserted = false;
1185
+
1186
+ for (let i = 0; i < buckets.length; i++) {
1187
+ // Start at a different bucket for each new file
1188
+ const bucket = buckets[(i + bucketOffset) % buckets.length];
1189
+ if (
1190
+ bucket.remainingSize >= file.sizeInBytes &&
1191
+ bucket.files.length < MAX_BUCKET_FILE_COUNT
1192
+ ) {
1193
+ bucket.files.push(file);
1194
+ bucket.remainingSize -= file.sizeInBytes;
1195
+ inserted = true;
1196
+ break;
1197
+ }
1198
+ }
1199
+
1200
+ if (!inserted) {
1201
+ buckets.push({
1202
+ files: [file],
1203
+ remainingSize: MAX_BUCKET_SIZE - file.sizeInBytes,
1204
+ });
1205
+ }
1206
+ bucketOffset++;
1207
+ }
1208
+
1209
+ let counter = fileMap.size - sortedFiles.length;
1122
1210
  const { rerender, unmount } = render(
1123
1211
  <Progress done={counter} total={fileMap.size} />
1124
1212
  );
1125
1213
 
1126
- fileMap.forEach((file: File, name: string) => {
1127
- const form = new FormData();
1128
- form.append(
1129
- "file",
1130
- new File([new Uint8Array(file.content.buffer)], name)
1131
- );
1214
+ const queue = new PQueue({ concurrency: BULK_UPLOAD_CONCURRENCY });
1132
1215
 
1133
- // TODO: Consider a retry
1216
+ for (const bucket of buckets) {
1217
+ // Don't upload empty buckets (can happen for tiny projects)
1218
+ if (bucket.files.length === 0) continue;
1134
1219
 
1135
- const promise = fetchResult<{ id: string }>(
1136
- `/accounts/${accountId}/pages/projects/${projectName}/file`,
1137
- {
1138
- method: "POST",
1139
- body: form,
1140
- }
1141
- ).then((response) => {
1142
- counter++;
1143
- rerender(<Progress done={counter} total={fileMap.size} />);
1144
- if (response.id != file.metadata.hash) {
1145
- throw new Error(
1146
- `Looks like there was an issue uploading '${name}'. Try again perhaps?`
1147
- );
1220
+ const payload: UploadPayloadFile[] = bucket.files.map((file) => ({
1221
+ key: file.hash,
1222
+ value: file.content,
1223
+ metadata: {
1224
+ contentType: file.contentType,
1225
+ },
1226
+ base64: true,
1227
+ }));
1228
+
1229
+ let attempts = 0;
1230
+ const doUpload = async (): Promise<void> => {
1231
+ try {
1232
+ return await fetchResult(`/pages/assets/upload`, {
1233
+ method: "POST",
1234
+ headers: {
1235
+ "Content-Type": "application/json",
1236
+ Authorization: `Bearer ${jwt}`,
1237
+ },
1238
+ body: JSON.stringify(payload),
1239
+ });
1240
+ } catch (e) {
1241
+ if (attempts < MAX_UPLOAD_ATTEMPTS) {
1242
+ // Linear backoff, 0 second first time, then 1 second etc.
1243
+ await new Promise((resolve) =>
1244
+ setTimeout(resolve, attempts++ * 1000)
1245
+ );
1246
+
1247
+ if ((e as { code: number }).code === 8000013) {
1248
+ // Looks like the JWT expired, fetch another one
1249
+ jwt = await fetchJwt();
1250
+ }
1251
+ return doUpload();
1252
+ } else {
1253
+ throw e;
1254
+ }
1148
1255
  }
1149
- });
1256
+ };
1150
1257
 
1151
- files.push(promise);
1152
- });
1258
+ queue.add(() =>
1259
+ doUpload().then(
1260
+ () => {
1261
+ counter += bucket.files.length;
1262
+ rerender(<Progress done={counter} total={fileMap.size} />);
1263
+ },
1264
+ (error) => {
1265
+ return Promise.reject(
1266
+ new FatalError(
1267
+ "Failed to upload files. Please try again.",
1268
+ error.code || 1
1269
+ )
1270
+ );
1271
+ }
1272
+ )
1273
+ );
1274
+ }
1153
1275
 
1154
- await Promise.all(files);
1276
+ await queue.onIdle();
1155
1277
 
1156
1278
  unmount();
1157
1279
 
@@ -1169,7 +1291,7 @@ const createDeployment: CommandModule<
1169
1291
  Object.fromEntries(
1170
1292
  [...fileMap.entries()].map(([fileName, file]) => [
1171
1293
  `/${fileName}`,
1172
- file.metadata.hash,
1294
+ file.hash,
1173
1295
  ])
1174
1296
  )
1175
1297
  )
@@ -1299,6 +1421,12 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1299
1421
  default: false,
1300
1422
  description: "Auto reload HTML pages when change is detected",
1301
1423
  },
1424
+ "node-compat": {
1425
+ describe: "Enable node.js compatibility",
1426
+ default: false,
1427
+ type: "boolean",
1428
+ hidden: true,
1429
+ },
1302
1430
  // TODO: Miniflare user options
1303
1431
  })
1304
1432
  .epilogue(pagesBetaWarning);
@@ -1313,6 +1441,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1313
1441
  kv: kvs = [],
1314
1442
  do: durableObjects = [],
1315
1443
  "live-reload": liveReload,
1444
+ "node-compat": nodeCompat,
1316
1445
  _: [_pages, _dev, ...remaining],
1317
1446
  }) => {
1318
1447
  // Beta message for `wrangler pages <commands>` usage
@@ -1346,7 +1475,16 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1346
1475
  );
1347
1476
 
1348
1477
  if (usingFunctions) {
1349
- const outfile = join(tmpdir(), "./functionsWorker.js");
1478
+ const outfile = join(
1479
+ tmpdir(),
1480
+ `./functionsWorker-${Math.random()}.js`
1481
+ );
1482
+
1483
+ if (nodeCompat) {
1484
+ console.warn(
1485
+ "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."
1486
+ );
1487
+ }
1350
1488
 
1351
1489
  logger.log(`Compiling worker to "${outfile}"...`);
1352
1490
 
@@ -1358,6 +1496,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1358
1496
  watch: true,
1359
1497
  onEnd: () => scriptReadyResolve(),
1360
1498
  buildOutputDirectory: directory,
1499
+ nodeCompat,
1361
1500
  });
1362
1501
  } catch {}
1363
1502
 
@@ -1372,6 +1511,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1372
1511
  watch: true,
1373
1512
  onEnd: () => scriptReadyResolve(),
1374
1513
  buildOutputDirectory: directory,
1514
+ nodeCompat,
1375
1515
  });
1376
1516
  });
1377
1517
 
@@ -1394,6 +1534,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1394
1534
  } else {
1395
1535
  logger.log("No functions. Shimming...");
1396
1536
  miniflareArgs = {
1537
+ // cfFetch sets the `cf` object that a function could expect
1538
+ // If there are no functions, there's no reason to set this up (and not make that network call)
1539
+ cfFetch: false,
1397
1540
  // TODO: The fact that these request/response hacks are necessary is ridiculous.
1398
1541
  // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
1399
1542
  script: `
@@ -1455,7 +1598,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1455
1598
 
1456
1599
  // env.ASSETS.fetch
1457
1600
  serviceBindings: {
1458
- async ASSETS(request: Request) {
1601
+ async ASSETS(request: MiniflareRequest) {
1459
1602
  if (proxyPort) {
1460
1603
  try {
1461
1604
  const url = new URL(request.url);
@@ -1579,6 +1722,12 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1579
1722
  type: "string",
1580
1723
  description: "The directory to output static assets to",
1581
1724
  },
1725
+ "node-compat": {
1726
+ describe: "Enable node.js compatibility",
1727
+ default: false,
1728
+ type: "boolean",
1729
+ hidden: true,
1730
+ },
1582
1731
  })
1583
1732
  .epilogue(pagesBetaWarning),
1584
1733
  async ({
@@ -1591,12 +1740,19 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1591
1740
  watch,
1592
1741
  plugin,
1593
1742
  "build-output-directory": buildOutputDirectory,
1743
+ "node-compat": nodeCompat,
1594
1744
  }) => {
1595
1745
  if (!isInPagesCI) {
1596
1746
  // Beta message for `wrangler pages <commands>` usage
1597
1747
  logger.log(pagesBetaWarning);
1598
1748
  }
1599
1749
 
1750
+ if (nodeCompat) {
1751
+ console.warn(
1752
+ "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."
1753
+ );
1754
+ }
1755
+
1600
1756
  buildOutputDirectory ??= dirname(outfile);
1601
1757
 
1602
1758
  await buildFunctions({
@@ -1609,6 +1765,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1609
1765
  watch,
1610
1766
  plugin,
1611
1767
  buildOutputDirectory,
1768
+ nodeCompat,
1612
1769
  });
1613
1770
  }
1614
1771
  )
@@ -1833,7 +1990,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1833
1990
  } as CommandModule);
1834
1991
  };
1835
1992
 
1836
- const invalidAssetsFetch: typeof fetch = () => {
1993
+ const invalidAssetsFetch: typeof miniflareFetch = () => {
1837
1994
  throw new Error(
1838
1995
  "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
1839
1996
  );
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) {