@cubing/dev-config 0.5.0 → 0.6.1

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/README.md CHANGED
@@ -114,3 +114,13 @@ The following are also available:
114
114
  - `./node_modules/@cubing/dev-config/ts/es2024-types/no-dom/tsconfig.json`
115
115
 
116
116
  This is useful for features like `Promise.withResolvers(…)`.
117
+
118
+ ### Checking `package.json` for a project
119
+
120
+ Run as follows:
121
+
122
+ ```shell
123
+ bun x --package @cubing/dev-config package.json check
124
+ ```
125
+
126
+ This checks both syntax and the presence of exports (and similar).
@@ -0,0 +1,582 @@
1
+ #!/usr/bin/env -S bun run --
2
+
3
+ /** biome-ignore-all lint/complexity/useLiteralKeys: https://github.com/biomejs/biome/discussions/7404 */
4
+
5
+ import assert from "node:assert";
6
+ import { constants } from "node:fs/promises";
7
+ import { argv, exit } from "node:process";
8
+ import type { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package";
9
+ import { semver } from "bun";
10
+ import { Path, ResolutionPrefix, stringifyIfPath } from "path-class";
11
+ import { PrintableShellCommand } from "printable-shell-command";
12
+
13
+ // Licenses from https://github.com/cubing/infra?tab=readme-ov-file#conventions
14
+ const PERMITTED_LICENSES = new Set([
15
+ "MPL-2.0",
16
+ "MIT",
17
+ "Unlicense",
18
+ "GPL-3.0-or-later",
19
+ ]);
20
+
21
+ // TODO: proper CLI parsing once this gets more complicated.
22
+ const subcommand: "check" | "format" = (() => {
23
+ const subcommand = argv[2];
24
+ if (!["check", "format"].includes(subcommand)) {
25
+ console.error("Must specify subcommand: `check` or `format`");
26
+ exit(1);
27
+ }
28
+ return subcommand as "check" | "format";
29
+ })();
30
+
31
+ let exitCode: number = 0;
32
+ let foundFixableErrors: boolean = false;
33
+
34
+ const PACKAGE_JSON_PATH = new Path("./package.json");
35
+
36
+ /*
37
+
38
+ Note: this checker is opinionated, and does not allow certain patterns.
39
+
40
+ It also assumes certain conventions about package structure and maintenance.
41
+
42
+ */
43
+
44
+ // TODO: support "format" command that corrects some things.
45
+ // TODO: Schema validation.
46
+
47
+ console.log("Parsing `package.json`:");
48
+ const packageJSONString = await PACKAGE_JSON_PATH.readText();
49
+ let packageJSON: JSONSchemaForNPMPackageJsonFiles = (() => {
50
+ try {
51
+ const packageJSON: JSONSchemaForNPMPackageJsonFiles =
52
+ JSON.parse(packageJSONString);
53
+ console.log("✅ `package.json` is valid JSON.");
54
+ return packageJSON;
55
+ } catch {
56
+ console.log(
57
+ "❌ `package.json` must be valid JSON (not JSONC or JSON5 or anything else).",
58
+ );
59
+ exit(1);
60
+ }
61
+ })();
62
+
63
+ console.log("Checking field order:");
64
+ const opinionatedFieldOrder = [
65
+ "name",
66
+ "version",
67
+ "homepage",
68
+ "description",
69
+ "author",
70
+ "license",
71
+ "repository",
72
+ "engines",
73
+ "os",
74
+ "cpu",
75
+ "type",
76
+ "main",
77
+ "types",
78
+ "module",
79
+ "browser",
80
+ "exports",
81
+ "bin",
82
+ "dependencies",
83
+ "devDependencies",
84
+ "optionalDependencies",
85
+ "peerDependencies",
86
+ "bundleDependencies",
87
+ "devEngines",
88
+ "files",
89
+ "scripts",
90
+ "keywords",
91
+ "@cubing/deploy",
92
+ "$schema",
93
+ ] as const;
94
+ const opinionatedFields = new Set(opinionatedFieldOrder);
95
+
96
+ const packageJSONOrder: string[] = [];
97
+ for (const key in packageJSON) {
98
+ // biome-ignore lint/suspicious/noExplicitAny: Type wrangling
99
+ if (opinionatedFields.has(key as any)) {
100
+ packageJSONOrder.push(key);
101
+ } else {
102
+ console.warn(`⚠️ [${JSON.stringify(key)}] Unexpected field.`);
103
+ }
104
+ }
105
+ const packageJSONByOpinionatedOrder: string[] = [];
106
+ for (const field of opinionatedFieldOrder) {
107
+ if (field in packageJSON) {
108
+ packageJSONByOpinionatedOrder.push(field);
109
+ }
110
+ }
111
+
112
+ try {
113
+ assert.deepEqual(packageJSONOrder, packageJSONByOpinionatedOrder);
114
+ console.log(`✅ Field order is good.`);
115
+ } catch {
116
+ switch (subcommand) {
117
+ case "check": {
118
+ console.log(`❌ Found opinionated fields out of order:`);
119
+ console.log(`↤ ${packageJSONOrder.join(", ")}`);
120
+ console.log("Expected:");
121
+ console.log(`↦ ${packageJSONByOpinionatedOrder.join(", ")}`);
122
+ console.log(
123
+ "📝 Run with the `sort` subcommand to sort. (Additional fields will kept after the field they previously followed.)",
124
+ );
125
+ foundFixableErrors = true;
126
+ exitCode = 1;
127
+ break;
128
+ }
129
+ case "format": {
130
+ console.log("📝 Invalid field order. Formatting…");
131
+ exitCode = 1;
132
+ const newKeyOrder: string[] = [];
133
+ for (const key of packageJSONByOpinionatedOrder) {
134
+ newKeyOrder.push(key);
135
+ }
136
+ for (const { value: key, previous } of withOrderingMetadata(
137
+ Object.keys(packageJSON),
138
+ )) {
139
+ if (newKeyOrder.includes(key)) {
140
+ continue;
141
+ }
142
+ if (!previous) {
143
+ newKeyOrder.unshift(key);
144
+ } else {
145
+ const { value: previousKey } = previous;
146
+ const idx = newKeyOrder.indexOf(previousKey);
147
+ newKeyOrder.splice(idx + 1, 0, key);
148
+ }
149
+ }
150
+ const newPackageJSON: JSONSchemaForNPMPackageJsonFiles = {};
151
+ for (const key of newKeyOrder) {
152
+ newPackageJSON[key] = packageJSON[key];
153
+ }
154
+ packageJSON = newPackageJSON;
155
+ break;
156
+ }
157
+ default:
158
+ throw new Error("Invalid command.") as never;
159
+ }
160
+ }
161
+
162
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof#description
163
+ type TypeOfType =
164
+ | "undefined"
165
+ | "object"
166
+ | "boolean"
167
+ | "number"
168
+ | "bigint"
169
+ | "string"
170
+ | "symbol"
171
+ | "function"
172
+ | "object";
173
+ type Categorization = "array" | "null" | TypeOfType;
174
+ // biome-ignore lint/suspicious/noExplicitAny: `any` is correct.
175
+ function categorize(v: any): Categorization {
176
+ if (Array.isArray(v)) {
177
+ return "array";
178
+ }
179
+ if (v === null) {
180
+ return "null";
181
+ }
182
+ return typeof v;
183
+ }
184
+
185
+ interface OrderingMetadata<T> {
186
+ value: T;
187
+ previous: { value: T } | null;
188
+ isLast: boolean;
189
+ }
190
+ function* withOrderingMetadata<T>(
191
+ iter: Iterable<T>,
192
+ ): Iterable<OrderingMetadata<T>> {
193
+ // The following functions as an `Option<T>`, even when `T` is undefined.
194
+ let previous: [OrderingMetadata<T>] | undefined;
195
+ for (const value of iter) {
196
+ if (previous) {
197
+ yield previous[0];
198
+ previous = [
199
+ { value, previous: { value: previous[0].value }, isLast: false },
200
+ ];
201
+ } else {
202
+ previous = [{ value, previous: null, isLast: false }];
203
+ }
204
+ }
205
+ if (previous) {
206
+ yield { ...previous[0], isLast: true };
207
+ }
208
+ }
209
+ type Breadcrumbs = (string | [string] | number)[];
210
+ function traverse<T>(
211
+ breadcrumbs: Breadcrumbs,
212
+ options?: { set?: T },
213
+ ): {
214
+ breadcrumbString: string;
215
+ maybeValue: [T] | null;
216
+ } {
217
+ assert(breadcrumbs.length > 0);
218
+ // biome-ignore lint/suspicious/noExplicitAny: Type wrangling
219
+ let maybeValue: [T | any] | null = [packageJSON];
220
+ let breadcrumbString = "";
221
+ for (let { value: breadcrumb, isLast } of withOrderingMetadata(breadcrumbs)) {
222
+ if (Array.isArray(breadcrumb)) {
223
+ assert(breadcrumb.length === 1);
224
+ assert(typeof breadcrumb[0] === "string");
225
+ breadcrumb = breadcrumb[0];
226
+ breadcrumbString += `[${JSON.stringify(breadcrumb)}]`;
227
+ } else if (typeof breadcrumb === "string") {
228
+ breadcrumbString += `.${breadcrumb}`;
229
+ } else {
230
+ breadcrumbString += `[${breadcrumb}]`;
231
+ }
232
+ if (options && "set" in options && isLast) {
233
+ if (
234
+ !maybeValue ||
235
+ !["array", "object"].includes(categorize(maybeValue[0]))
236
+ ) {
237
+ // This okay for now, because we currently only write to values we have read.
238
+ throw new Error(
239
+ "Missing (but expected) traversal path while setting a value",
240
+ ) as never;
241
+ }
242
+ maybeValue[0][breadcrumb] = stringifyIfPath(options.set);
243
+ } else if (
244
+ maybeValue &&
245
+ ["array", "object"].includes(categorize(maybeValue[0])) &&
246
+ breadcrumb in maybeValue[0]
247
+ ) {
248
+ maybeValue = [maybeValue[0][breadcrumb]];
249
+ } else {
250
+ maybeValue = null;
251
+ }
252
+ }
253
+ return { breadcrumbString, maybeValue };
254
+ }
255
+
256
+ function field<T>(
257
+ breadcrumbs: Breadcrumbs,
258
+ type: Categorization | Categorization[],
259
+ options?: {
260
+ optional?: boolean;
261
+ additionalChecks?: { [requirementMessage: string]: (t: T) => boolean };
262
+ skipPrintingSuccess?: boolean;
263
+ },
264
+ ) {
265
+ const { breadcrumbString, maybeValue } = traverse(breadcrumbs);
266
+ if (!maybeValue) {
267
+ if (options?.optional) {
268
+ if (!options.skipPrintingSuccess) {
269
+ console.log(`☑️ ${breadcrumbString}`);
270
+ }
271
+ return;
272
+ } else {
273
+ console.log(`❌ ${breadcrumbString} — Must be present.`);
274
+ exitCode = 1;
275
+ return;
276
+ }
277
+ }
278
+ const [value] = maybeValue;
279
+
280
+ const typeArray = Array.isArray(type) ? type : [type];
281
+ const category = categorize(value);
282
+ if (typeArray.includes(category)) {
283
+ for (const [failureMessage, fn] of Object.entries(
284
+ options?.additionalChecks ?? {},
285
+ )) {
286
+ if (!fn) {
287
+ console.log(`❌ ${breadcrumbString} | ${failureMessage}`);
288
+ exitCode = 1;
289
+ return;
290
+ }
291
+ }
292
+ if (!options?.skipPrintingSuccess) {
293
+ console.log(`✅ ${breadcrumbString}`);
294
+ }
295
+ } else {
296
+ if (category === "undefined") {
297
+ console.log(`❌ ${breadcrumbString} — Must be present.`);
298
+ } else if (type === "undefined") {
299
+ console.log(`❌ ${breadcrumbString} — Present (but must not be).`);
300
+ } else {
301
+ if (Array.isArray(type)) {
302
+ console.log(
303
+ `❌ ${breadcrumbString} — Does not match an expected type: ${type.join(", ")}`,
304
+ );
305
+ } else {
306
+ console.log(
307
+ `❌ ${breadcrumbString} — Does not match expected type: ${type}`,
308
+ );
309
+ }
310
+ }
311
+ exitCode = 1;
312
+ return;
313
+ }
314
+ }
315
+
316
+ function mustNotBePresent(breadcrumbs: Breadcrumbs) {
317
+ const { breadcrumbString, maybeValue } = traverse(breadcrumbs);
318
+ if (maybeValue) {
319
+ console.log(`❌ ${breadcrumbString} — Must not be present.`);
320
+ exitCode = 1;
321
+ return;
322
+ }
323
+ }
324
+
325
+ console.log("Checking presence and type of fields:");
326
+
327
+ field(["name"], "string");
328
+ field(["version"], "string", {
329
+ additionalChecks: {
330
+ "Version cannot be parsed.": (version: string) =>
331
+ semver.order(version, version) === 0,
332
+ },
333
+ });
334
+ field(["homepage"], "string", { optional: true });
335
+ field(["description"], "string");
336
+ // TODO: format author.
337
+ field(["author"], ["string", "object"]);
338
+ if (categorize(packageJSON["author"]) === "object") {
339
+ field(["author", "name"], "string");
340
+ field(["author", "email"], "string");
341
+ field(["author", "url"], "string", {
342
+ additionalChecks: {
343
+ "URL must parse.": (url: string) => {
344
+ try {
345
+ new URL(url);
346
+ return true;
347
+ } catch {
348
+ return false;
349
+ }
350
+ },
351
+ },
352
+ });
353
+ }
354
+ field(["license"], "string", {
355
+ additionalChecks: {
356
+ "Must contain a non-permitted license.": (license: string) => {
357
+ for (const licenseEntry of license.split(" OR ")) {
358
+ if (!PERMITTED_LICENSES.has(licenseEntry)) {
359
+ return false;
360
+ }
361
+ }
362
+ return true;
363
+ },
364
+ },
365
+ });
366
+ // TODO: format repo.
367
+ field(["repository"], "object");
368
+ field(["repository", "type"], "string");
369
+ const GIT_URL_PREFIX = "git+";
370
+ const GIT_URL_SUFFIX = ".";
371
+ field(["repository", "url"], "string", {
372
+ additionalChecks: {
373
+ [`URL must be prefixed with \`${GIT_URL_PREFIX}\`.`]: (url: string) =>
374
+ url.startsWith(GIT_URL_PREFIX),
375
+ [`URL must end with with \`.${GIT_URL_SUFFIX}\`.`]: (url: string) =>
376
+ url.endsWith(GIT_URL_SUFFIX),
377
+ "URL must parse.": (url: string) => {
378
+ try {
379
+ new URL(url.slice());
380
+ return true;
381
+ } catch {
382
+ return false;
383
+ }
384
+ },
385
+ },
386
+ });
387
+ // TODO: Validate version range syntax.
388
+ field(["engines"], "object", { optional: true });
389
+ field(["os"], "array", { optional: true });
390
+ field(["cpu"], "array", { optional: true });
391
+ field(["type"], "string", {
392
+ additionalChecks: {
393
+ 'Type must be `"module"`.': (type: string) => type === "module",
394
+ },
395
+ });
396
+ field(["exports"], "object");
397
+ field(["bin"], "object", { optional: true });
398
+ field(["dependencies"], "object", { optional: true });
399
+ field(["devDependencies"], "object", { optional: true });
400
+ field(["optionalDependencies"], "object", { optional: true });
401
+ field(["peerDependencies"], "object", { optional: true });
402
+ field(["bundleDependencies"], "object", { optional: true });
403
+ field(["devEngines"], "object", { optional: true });
404
+ // TODO: check for path resolution prefix?
405
+ // Set to `["*"]` if needed.
406
+ field(["files"], "array");
407
+ field(["scripts"], "object");
408
+ // Set to `"# no-op"` if needed.
409
+ field(["scripts", "prepublishOnly"], "string");
410
+ if ("main" in packageJSON || "types" in packageJSON) {
411
+ field(["main"], "string");
412
+ field(["types"], "string");
413
+ } else {
414
+ console.log("☑️ .main");
415
+ console.log("☑️ .types");
416
+ }
417
+ mustNotBePresent(["module"]);
418
+ mustNotBePresent(["browser"]);
419
+
420
+ console.log("Checking paths of binaries and exports:");
421
+
422
+ const tempDir = await Path.makeTempDir();
423
+ await using _ = {
424
+ [Symbol.asyncDispose]: () => tempDir.rm_rf(),
425
+ };
426
+ const extractionDir = await tempDir.join("extracted").mkdir();
427
+ const data: { filename: string }[] = await new PrintableShellCommand("npm", [
428
+ "pack",
429
+ "--json",
430
+ "--ignore-scripts",
431
+ ["--pack-destination", tempDir],
432
+ ]).json();
433
+ const tgzPath = tempDir.join(data[0].filename);
434
+ await new PrintableShellCommand("tar", [
435
+ ["-C", extractionDir],
436
+ ["-xvzf", tgzPath],
437
+ ]).spawn().success;
438
+
439
+ const extractedRoot = extractionDir.join("package/");
440
+ assert(await extractedRoot.existsAsDir());
441
+
442
+ const checks: Promise<string>[] = [];
443
+
444
+ // TODO: check compilability
445
+ function checkPath(
446
+ breadcrumbs: Breadcrumbs,
447
+ options: { expectPrefix: ResolutionPrefix; mustBeExecutable?: true },
448
+ ) {
449
+ const { breadcrumbString, maybeValue } = traverse(breadcrumbs);
450
+ if (!maybeValue) {
451
+ return;
452
+ }
453
+ const [value] = maybeValue;
454
+ checks.push(
455
+ (async () => {
456
+ if (typeof value !== "string") {
457
+ exitCode = 1;
458
+ return `❌ ${breadcrumbString} — Non-string value`;
459
+ }
460
+ if (value.includes("*")) {
461
+ return `⏭️ ${breadcrumbString} — Skipping due to glob (*) — ${value}`;
462
+ }
463
+ const unresolvedPath = new Path(value);
464
+ if (unresolvedPath.resolutionPrefix !== options.expectPrefix) {
465
+ if (unresolvedPath.resolutionPrefix === ResolutionPrefix.Absolute) {
466
+ exitCode = 1;
467
+ return `❌ ${breadcrumbString} — Incorrect resolution prefix (${unresolvedPath.resolutionPrefix}) — ${value}`;
468
+ } else {
469
+ switch (subcommand) {
470
+ case "check": {
471
+ exitCode = 1;
472
+ foundFixableErrors = true;
473
+ return `❌ ${breadcrumbString} — Incorrect resolution prefix (${unresolvedPath.resolutionPrefix}) — 📝 fixable! — ${value}`;
474
+ }
475
+ case "format": {
476
+ console.log(
477
+ `📝 — Incorrect resolution prefix (${unresolvedPath.resolutionPrefix}) — fixing! — ${value}`,
478
+ );
479
+ // TODO: do this calculation before reporting as fixable
480
+ const newPath =
481
+ options.expectPrefix === ResolutionPrefix.Bare
482
+ ? unresolvedPath.asBare()
483
+ : unresolvedPath.asRelative();
484
+ traverse(breadcrumbs, { set: newPath });
485
+ break;
486
+ }
487
+ default:
488
+ throw new Error("Invalid command.") as never;
489
+ }
490
+ }
491
+ }
492
+ if (
493
+ unresolvedPath.path.startsWith("../") ||
494
+ unresolvedPath.path === ".."
495
+ ) {
496
+ exitCode = 1;
497
+ return `❌ ${breadcrumbString} — Invalid traversal of parent path. — ${value}`;
498
+ }
499
+ const resolvedPath = Path.resolve(unresolvedPath, extractedRoot);
500
+ // TODO: allow folders (with a required trailing slash)?
501
+ if (!(await resolvedPath.existsAsFile())) {
502
+ exitCode = 1;
503
+ return `❌ ${breadcrumbString} — Missing — ${value}`;
504
+ }
505
+ if (options.mustBeExecutable) {
506
+ if (!((await resolvedPath.stat()).mode ^ constants.X_OK)) {
507
+ // This is not considered fixable because the binary may be the output
508
+ // of a build process. In that case, the build process is responsible
509
+ // for marking it as executable.
510
+ return `❌ ${breadcrumbString} — Must be executable — ${value}`;
511
+ }
512
+ }
513
+ return `✅ ${breadcrumbString} — OK — ${value}`;
514
+ })(),
515
+ );
516
+ }
517
+
518
+ checkPath(["main"], { expectPrefix: ResolutionPrefix.Relative });
519
+ checkPath(["types"], { expectPrefix: ResolutionPrefix.Relative });
520
+ checkPath(["module"], { expectPrefix: ResolutionPrefix.Relative });
521
+ checkPath(["browser"], { expectPrefix: ResolutionPrefix.Relative });
522
+
523
+ if (packageJSON.exports) {
524
+ for (const [exportPath, value] of Object.entries(packageJSON.exports)) {
525
+ if (!value) {
526
+ // biome-ignore lint/complexity/noUselessContinue: Explicit control flow.
527
+ continue;
528
+ } else if (typeof value === "string") {
529
+ checkPath(["exports", [exportPath]], {
530
+ expectPrefix: ResolutionPrefix.Relative,
531
+ });
532
+ } else if (value === null) {
533
+ // biome-ignore lint/complexity/noUselessContinue: Explicit control flow.
534
+ continue;
535
+ } else if (Array.isArray(value)) {
536
+ throw new Error(
537
+ "❌ .exports — Must use an object (instead of an array).",
538
+ );
539
+ } else {
540
+ for (const secondaryKey of Object.keys(value as Record<string, string>)) {
541
+ checkPath(["exports", [exportPath], secondaryKey], {
542
+ expectPrefix: ResolutionPrefix.Relative,
543
+ });
544
+ }
545
+ }
546
+ }
547
+ }
548
+
549
+ const { bin } = packageJSON;
550
+ if (bin) {
551
+ for (const binEntry of Object.keys(bin as Record<string, string>)) {
552
+ checkPath(["bin", [binEntry]], {
553
+ // `npm pkg fix` prefers bare paths for `bin` entries for some reason. 🤷
554
+ expectPrefix: ResolutionPrefix.Bare,
555
+ // `npm` will technically make binary entry points executable, but we want
556
+ // to enforce that the unpackaged path also is. This is particularly
557
+ // important when the package is linked.
558
+ mustBeExecutable: true,
559
+ });
560
+ }
561
+ }
562
+
563
+ console.log((await Promise.all(checks)).join("\n"));
564
+
565
+ if (subcommand === "format") {
566
+ console.log("📝 Writing formatting fixes.");
567
+ // TODO: support trailing space in `path-class`.
568
+ await PACKAGE_JSON_PATH.write(`${JSON.stringify(packageJSON, null, " ")}\n`);
569
+ console.log(PACKAGE_JSON_PATH.path);
570
+ console.log("📝 Running `npm pkg fix.");
571
+ await new PrintableShellCommand("npm", ["pkg", "fix"])
572
+ .print({ argumentLineWrapping: "inline" })
573
+ .spawn().success;
574
+ } else if (foundFixableErrors) {
575
+ console.log();
576
+ console.log(
577
+ "📝 Found fixable errors. Run with the `format` subcommand to fix.",
578
+ );
579
+ console.log();
580
+ }
581
+
582
+ exit(exitCode);
package/package.json CHANGED
@@ -1,27 +1,45 @@
1
1
  {
2
2
  "name": "@cubing/dev-config",
3
- "version": "0.5.0",
4
- "type": "module",
5
- "dependencies": {
6
- "esbuild": ">=0.25.3 && <1.0.0"
3
+ "version": "0.6.1",
4
+ "description": "Common dev configs for projects.",
5
+ "author": {
6
+ "name": "Lucas Garron",
7
+ "email": "code@garron.net",
8
+ "url": "https://garron.net/"
7
9
  },
8
- "devDependencies": {
9
- "@biomejs/biome": "^2.3.2",
10
- "@types/bun": "^1.3.1",
11
- "typescript": "^5.9.3"
10
+ "license": "MPL-2.0",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/cubing/dev-config.git"
12
14
  },
15
+ "type": "module",
13
16
  "exports": {
14
17
  "./esbuild/es2022": {
15
18
  "types": "./esbuild/es2022/index.d.ts",
16
19
  "import": "./esbuild/es2022/index.js"
17
20
  }
18
21
  },
19
- "scripts": {
20
- "prepublishOnly": "make prepublishOnly"
22
+ "bin": {
23
+ "package.json": "bin/package.json.ts"
24
+ },
25
+ "dependencies": {
26
+ "esbuild": ">=0.25.3 && <1.0.0",
27
+ "path-class": ">=0.10.8",
28
+ "printable-shell-command": "^3.0.2"
29
+ },
30
+ "devDependencies": {
31
+ "@biomejs/biome": "^2.3.10",
32
+ "@schemastore/package": "^0.0.10",
33
+ "@types/bun": "^1.3.5",
34
+ "@types/node": "^25.0.3",
35
+ "typescript": "^5.9.3"
21
36
  },
22
37
  "files": [
23
38
  "./biome/",
24
39
  "./esbuild/",
25
40
  "./ts/"
26
- ]
41
+ ],
42
+ "scripts": {
43
+ "prepublishOnly": "make prepublishOnly"
44
+ }
27
45
  }