mdat 3.0.0 → 3.2.0

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/dist/bin/cli.js CHANGED
@@ -29,7 +29,7 @@ import { confirm, group, intro, note, outro, select } from "@clack/prompts";
29
29
 
30
30
  //#region package.json
31
31
  var name = "mdat";
32
- var version = "3.0.0";
32
+ var version = "3.2.0";
33
33
 
34
34
  //#endregion
35
35
  //#region src/lib/deep-merge-defined.ts
@@ -41,7 +41,8 @@ function stripUndefinedDeep(object) {
41
41
  }, {});
42
42
  }
43
43
  function deepMergeDefined(...objects) {
44
- return deepmerge(...objects.map((v, i) => i === 0 ? v : stripUndefinedDeep(v)));
44
+ const stripped = objects.map((v, i) => i === 0 ? v : stripUndefinedDeep(v));
45
+ return deepmerge(...stripped);
45
46
  }
46
47
 
47
48
  //#endregion
@@ -111,6 +112,7 @@ async function getContextMetadata() {
111
112
  "codemetaJson",
112
113
  "gitConfig",
113
114
  "githubActions",
115
+ "gitStats",
114
116
  "goGoMod",
115
117
  "goGoreleaserYaml",
116
118
  "javaPomXml",
@@ -140,8 +142,9 @@ async function getContextMetadata() {
140
142
  const GIT_PREFIX_REGEX = /^git\+/v;
141
143
  const GIT_SUFFIX_REGEX = /\.git$/v;
142
144
  const TRAILING_SLASH_REGEX = /\/$/v;
145
+ const PACKAGE_MANAGER_REGEX = /^(?<name>[^@]+)@(?<version>[^+]+)/v;
143
146
  const readmeMetadataTemplate = defineTemplate((context) => {
144
- const { githubActions, licenseFile, metascope, nodePackageJson } = context;
147
+ const { githubActions, gitStats, licenseFile, metascope, nodePackageJson } = context;
145
148
  const codemeta = templates.codemetaJson(context, {});
146
149
  const nodePackage = helpers.firstOf(nodePackageJson)?.data;
147
150
  const licenseFileData = helpers.firstOf(licenseFile);
@@ -174,6 +177,27 @@ const readmeMetadataTemplate = defineTemplate((context) => {
174
177
  version
175
178
  }));
176
179
  })();
180
+ const developmentDependencies = (() => {
181
+ if (nodePackage === void 0) return;
182
+ const runtimes = helpers.ensureArray(nodePackage.devEngines?.runtime).map(({ name, version }) => ({
183
+ name,
184
+ version
185
+ }));
186
+ const packageManagers = helpers.ensureArray(nodePackage.devEngines?.packageManager).map(({ name, version }) => ({
187
+ name,
188
+ version
189
+ }));
190
+ const pinnedPackageManager = nodePackage.packageManager === void 0 ? void 0 : PACKAGE_MANAGER_REGEX.exec(nodePackage.packageManager)?.groups;
191
+ if (pinnedPackageManager?.name !== void 0 && pinnedPackageManager.version !== void 0 && packageManagers.every((entry) => entry.name !== pinnedPackageManager.name)) packageManagers.push({
192
+ name: pinnedPackageManager.name,
193
+ version: pinnedPackageManager.version
194
+ });
195
+ if (runtimes.length === 0 && packageManagers.length === 0) return;
196
+ return {
197
+ packageManagers: packageManagers.length > 0 ? packageManagers : void 0,
198
+ runtimes: runtimes.length > 0 ? runtimes : void 0
199
+ };
200
+ })();
177
201
  const firstAuthor = helpers.firstOf(helpers.ensureArray(codemeta.author));
178
202
  return {
179
203
  author: helpers.firstOf(helpers.mixedStringsToArray(helpers.toBasicNames(codemeta.author))),
@@ -181,6 +205,7 @@ const readmeMetadataTemplate = defineTemplate((context) => {
181
205
  bin,
182
206
  ciActionFileName: ciActionFilePath === void 0 ? void 0 : path.basename(ciActionFilePath),
183
207
  description: codemeta.description,
208
+ developmentDependencies,
184
209
  engines,
185
210
  isPublicNpmPackage: nodePackage?.private !== true && (nodePackage?.name.startsWith("@") && nodePackage.publishConfig?.access === "public" || true),
186
211
  issuesUrl: codemeta.issueTracker,
@@ -193,6 +218,7 @@ const readmeMetadataTemplate = defineTemplate((context) => {
193
218
  projectDirectory: metascope?.data.options.path === void 0 ? void 0 : `file://${metascope.data.options.path}`,
194
219
  repositoryUrl: repoUrl,
195
220
  runtimePlatform: codemeta.runtimePlatform,
221
+ usesGitLfs: helpers.firstOf(gitStats)?.data.hasLfs === true,
196
222
  usesPnpm: helpers.usesPnpm(nodePackageJson)
197
223
  };
198
224
  });
@@ -220,13 +246,14 @@ var badges_default = { badges: { async content(options) {
220
246
  npm: z.array(z.string()).optional()
221
247
  }).optional().parse(options);
222
248
  const metadata = await getReadmeMetadata();
223
- const { ciActionFileName, license, licenseUrl, name, repositoryUrl } = metadata;
249
+ const { ciActionFileName, license, licenseUrl, name, repositoryUrl, usesGitLfs } = metadata;
224
250
  const badges = [];
225
251
  if (validOptions?.npm === void 0) {
226
252
  if (metadata.isPublicNpmPackage) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://www.npmjs.com/package/${name})`);
227
253
  } else for (const packageName of validOptions.npm) badges.push(`[![NPM Package ${packageName}](https://img.shields.io/npm/v/${packageName}.svg)](https://www.npmjs.com/package/${packageName})`);
228
254
  if (license !== void 0 && licenseUrl !== void 0) badges.push(`[![License: ${license}](https://img.shields.io/badge/License-${license.replaceAll("-", "--")}-yellow.svg)](${licenseUrl})`);
229
255
  if (ciActionFileName !== void 0 && repositoryUrl !== void 0) badges.push(`[![CI](${repositoryUrl}/actions/workflows/${ciActionFileName}/badge.svg)](${repositoryUrl}/actions/workflows/${ciActionFileName})`);
256
+ if (usesGitLfs) badges.push("![Git LFS](https://img.shields.io/badge/Git%20LFS-enabled-F64935?logo=gitlfs&logoColor=white)");
230
257
  if (validOptions?.custom !== void 0) for (const [badgeName, { image, link }] of Object.entries(validOptions.custom)) badges.push(`[![${badgeName}](${image})](${link})`);
231
258
  return badges.join("\n");
232
259
  } } };
@@ -349,7 +376,7 @@ const PLATFORM_INFO = {
349
376
  },
350
377
  node: {
351
378
  display: "Node.js",
352
- url: "https://nodejs.org/"
379
+ url: "https://nodejs.org/en"
353
380
  },
354
381
  python: {
355
382
  display: "Python",
@@ -409,6 +436,58 @@ var description_default = { description: { async content() {
409
436
  return `**${description}**`;
410
437
  } } };
411
438
 
439
+ //#endregion
440
+ //#region src/lib/readme/rules/development-dependencies.ts
441
+ const TOOL_INFO = {
442
+ bun: {
443
+ display: "Bun",
444
+ url: "https://bun.sh/"
445
+ },
446
+ deno: {
447
+ display: "Deno",
448
+ url: "https://deno.land/"
449
+ },
450
+ node: {
451
+ display: "Node.js",
452
+ url: "https://nodejs.org/en"
453
+ },
454
+ npm: {
455
+ display: "npm",
456
+ url: "https://www.npmjs.com/"
457
+ },
458
+ pnpm: {
459
+ display: "pnpm",
460
+ url: "https://pnpm.io/"
461
+ },
462
+ yarn: {
463
+ display: "Yarn",
464
+ url: "https://yarnpkg.com/"
465
+ }
466
+ };
467
+ var development_dependencies_default = { "development-dependencies": { async content() {
468
+ const { developmentDependencies } = await getReadmeMetadata();
469
+ if (developmentDependencies === void 0) return "";
470
+ const { packageManagers, runtimes } = developmentDependencies;
471
+ const items = [];
472
+ for (const { name, version } of [...runtimes ?? [], ...packageManagers ?? []]) {
473
+ const info = TOOL_INFO[name.toLowerCase()];
474
+ const display = info ? `[${info.display}](${info.url})` : name;
475
+ items.push(version === void 0 || version === "" ? `- ${display}` : `- ${display} ${version}`);
476
+ }
477
+ return [
478
+ "## Development Dependencies",
479
+ "",
480
+ ...items
481
+ ].join("\n");
482
+ } } };
483
+
484
+ //#endregion
485
+ //#region src/lib/readme/rules/dev-dependencies.ts
486
+ /**
487
+ * Simple alias for `development-dependencies`
488
+ */
489
+ var dev_dependencies_default = { "dev-dependencies": getSoleRule(development_dependencies_default) };
490
+
412
491
  //#endregion
413
492
  //#region src/lib/readme/rules/license.ts
414
493
  var license_default = { license: { async content() {
@@ -492,11 +571,11 @@ const gzipCompressAsync = promisify(gzip);
492
571
  * @param bytes - Size in bytes
493
572
  * @param originalSize - Original file size for percentage calculation
494
573
  */
495
- function createSizeInfo(bytes, originalSize) {
574
+ function createSizeInfo(bytes, originalSize, precision) {
496
575
  const percent = originalSize === 0 ? 0 : (originalSize - bytes) / originalSize * 100;
497
576
  return {
498
577
  bytes,
499
- bytesPretty: prettyBytes(bytes, { maximumFractionDigits: 1 }),
578
+ bytesPretty: prettyBytes(bytes, { maximumFractionDigits: precision }),
500
579
  percent,
501
580
  percentPretty: `${Math.round(percent)}%`
502
581
  };
@@ -505,19 +584,20 @@ function createSizeInfo(bytes, originalSize) {
505
584
  * Analyzes a file's size and its compressed sizes using Brotli and Gzip
506
585
  *
507
586
  * @param filePath - Path to the file to analyze
587
+ * @param precision - Maximum number of decimal places in formatted byte sizes
508
588
  *
509
589
  * @returns Promise containing detailed size report
510
590
  * @throws {Error} If file cannot be read or compressed
511
591
  */
512
- async function createSizeReport(filePath) {
592
+ async function createSizeReport(filePath, precision = 1) {
513
593
  try {
514
594
  const fileContent = await fs.readFile(filePath);
515
595
  const originalSize = fileContent.length;
516
596
  const [brotliCompressed, gzipCompressed] = await Promise.all([brotliCompressAsync(fileContent), gzipCompressAsync(fileContent)]);
517
597
  return {
518
- brotli: createSizeInfo(brotliCompressed.length, originalSize),
519
- gzip: createSizeInfo(gzipCompressed.length, originalSize),
520
- original: createSizeInfo(originalSize, originalSize)
598
+ brotli: createSizeInfo(brotliCompressed.length, originalSize, precision),
599
+ gzip: createSizeInfo(gzipCompressed.length, originalSize, precision),
600
+ original: createSizeInfo(originalSize, originalSize, precision)
521
601
  };
522
602
  } catch (error) {
523
603
  throw new Error(`Failed to analyze file: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
@@ -543,7 +623,8 @@ function getSizeForCompression(report, compression) {
543
623
  }
544
624
  var size_default = { size: { async content(options) {
545
625
  const validOptions = optionsSchema$1.parse(options);
546
- return getSizeForCompression(await createSizeReport(path.join(process.cwd(), validOptions.file)), validOptions.compression);
626
+ const fullPath = path.join(process.cwd(), validOptions.file);
627
+ return getSizeForCompression(await createSizeReport(fullPath), validOptions.compression);
547
628
  } } };
548
629
 
549
630
  //#endregion
@@ -554,12 +635,14 @@ const optionsSchema = z.union([z.object({
554
635
  file: fileSchema,
555
636
  gzip: z.boolean().optional().default(true),
556
637
  original: z.boolean().optional().default(true),
638
+ precision: z.number().int().min(0).max(100).optional().default(0),
557
639
  showPercentage: z.boolean().optional().default(false)
558
640
  }), z.object({
559
641
  brotli: z.boolean().optional().default(true),
560
642
  files: fileSchema,
561
643
  gzip: z.boolean().optional().default(true),
562
644
  original: z.boolean().optional().default(true),
645
+ precision: z.number().int().min(0).max(100).optional().default(0),
563
646
  showPercentage: z.boolean().optional().default(false)
564
647
  })]).transform((options) => {
565
648
  if ("file" in options) {
@@ -593,7 +676,8 @@ function formatMarkdownTable(reports, options) {
593
676
  var size_table_default = { "size-table": { async content(options) {
594
677
  const validOptions = optionsSchema.parse(options);
595
678
  return formatMarkdownTable(await Promise.all(validOptions.files.map(async (file) => {
596
- return [file, await createSizeReport(path.join(process.cwd(), file))];
679
+ const fullPath = path.join(process.cwd(), file);
680
+ return [file, await createSizeReport(fullPath, validOptions.precision)];
597
681
  })), {
598
682
  brotli: validOptions.brotli,
599
683
  gzip: validOptions.gzip,
@@ -606,16 +690,17 @@ var size_table_default = { "size-table": { async content(options) {
606
690
  //#region src/lib/readme/rules/table-of-contents.ts
607
691
  var table_of_contents_default = { "table-of-contents": {
608
692
  async content(options, { tree }) {
693
+ const validOptions = z.object({ depth: z.union([
694
+ z.literal(1),
695
+ z.literal(2),
696
+ z.literal(3),
697
+ z.literal(4),
698
+ z.literal(5),
699
+ z.literal(6)
700
+ ]).optional() }).optional().parse(options);
609
701
  const result = toc(tree, {
610
702
  heading: null,
611
- maxDepth: z.object({ depth: z.union([
612
- z.literal(1),
613
- z.literal(2),
614
- z.literal(3),
615
- z.literal(4),
616
- z.literal(5),
617
- z.literal(6)
618
- ]).optional() }).optional().parse(options)?.depth ?? 3,
703
+ maxDepth: validOptions?.depth ?? 3,
619
704
  tight: true
620
705
  });
621
706
  if (result.map === void 0) throw new Error("Could not generate table of contents");
@@ -645,6 +730,8 @@ var rules_default = {
645
730
  ...contributing_default,
646
731
  ...dependencies_default,
647
732
  ...description_default,
733
+ ...dev_dependencies_default,
734
+ ...development_dependencies_default,
648
735
  ...footer_default,
649
736
  ...header_default,
650
737
  ...install_default,
@@ -700,11 +787,13 @@ async function loadConfig(options) {
700
787
  let loaded;
701
788
  if (typeof configOrPath === "string") {
702
789
  let loadResults;
703
- if (path.basename(configOrPath).endsWith("package.json")) loadResults = {
704
- config: mdatJsonLoader(configOrPath, await fs.readFile(configOrPath, "utf8")),
705
- filepath: configOrPath
706
- };
707
- else loadResults = await getAdditionalConfigExplorer().load(configOrPath);
790
+ if (path.basename(configOrPath).endsWith("package.json")) {
791
+ const packageJson = await fs.readFile(configOrPath, "utf8");
792
+ loadResults = {
793
+ config: mdatJsonLoader(configOrPath, packageJson),
794
+ filepath: configOrPath
795
+ };
796
+ } else loadResults = await getAdditionalConfigExplorer().load(configOrPath);
708
797
  if (loadResults === null || loadResults === void 0) continue;
709
798
  const { config: loadedConfig, filepath } = loadResults;
710
799
  log.debug(`Loaded additional config from "${filepath}"`);
@@ -867,7 +956,8 @@ function stripLintPlugins(config) {
867
956
  //#region src/lib/processors.ts
868
957
  async function processFiles(files, loader, processorGetter, name, output, config) {
869
958
  const [resolvedConfig, localRemarkConfig] = await Promise.all([loader({ additionalConfig: config }), loadAmbientRemarkConfig()]);
870
- const inputOutputPaths = await getInputOutputPaths(ensureArray(files), output, name, "md");
959
+ const resolvedFiles = ensureArray(files);
960
+ const inputOutputPaths = await getInputOutputPaths(resolvedFiles, output, name, "md");
871
961
  const resolvedProcessor = processorGetter(resolvedConfig, localRemarkConfig);
872
962
  return await Promise.all(inputOutputPaths.map(async ({ input, name: outputName, output: outputDirectory }) => {
873
963
  const inputFile = await read(input);
@@ -1222,7 +1312,8 @@ try {
1222
1312
  output,
1223
1313
  print
1224
1314
  });
1225
- const results = await expand(files, fileName, output, collectConfig(config), { format });
1315
+ const mergedConfig = collectConfig(config);
1316
+ const results = await expand(files, fileName, output, mergedConfig, { format });
1226
1317
  for (const file of results) if (print) process.stdout.write(file.toString());
1227
1318
  else await write(file);
1228
1319
  reporterMdat(results);
@@ -1253,7 +1344,8 @@ try {
1253
1344
  log.debug(`Stripped comments in ${prettyMilliseconds(performance.now() - startTime)}.`);
1254
1345
  process.exitCode = getExitCode(results);
1255
1346
  }).command("check [files..] [options]", "Check if MDAT placeholder comments are up to date. Exits with code 1 if any files have stale or unexpanded content.", (commandYargs) => commandYargs.positional(...filesPositional).option(configOption).option(formatOption), async ({ config, files, format }) => {
1256
- const results = await check(files, collectConfig(config), { format });
1347
+ const mergedConfig = collectConfig(config);
1348
+ const results = await check(files, mergedConfig, { format });
1257
1349
  let allInSync = true;
1258
1350
  for (const { inSync, result } of results) {
1259
1351
  const filePath = result.path === "" ? "unknown" : result.path;
@@ -156,6 +156,16 @@ declare const readmeMetadataTemplate: import("metascope").Template<{
156
156
  bin: string[] | undefined;
157
157
  ciActionFileName: string | undefined;
158
158
  description: string | undefined;
159
+ developmentDependencies: {
160
+ packageManagers: {
161
+ name: string;
162
+ version: string | undefined;
163
+ }[] | undefined;
164
+ runtimes: {
165
+ name: string;
166
+ version: string | undefined;
167
+ }[] | undefined;
168
+ } | undefined;
159
169
  engines: {
160
170
  [k: string]: string;
161
171
  } | undefined;
@@ -174,6 +184,7 @@ declare const readmeMetadataTemplate: import("metascope").Template<{
174
184
  projectDirectory: string | undefined;
175
185
  repositoryUrl: string | undefined;
176
186
  runtimePlatform: string[] | undefined;
187
+ usesGitLfs: boolean;
177
188
  usesPnpm: boolean;
178
189
  }>;
179
190
  type ReadmeMetadata = ReturnType<typeof readmeMetadataTemplate>;
@@ -188,6 +199,16 @@ declare function getReadmeMetadata(): Promise<{
188
199
  bin: string[] | undefined;
189
200
  ciActionFileName: string | undefined;
190
201
  description: string | undefined;
202
+ developmentDependencies: {
203
+ packageManagers: {
204
+ name: string;
205
+ version: string | undefined;
206
+ }[] | undefined;
207
+ runtimes: {
208
+ name: string;
209
+ version: string | undefined;
210
+ }[] | undefined;
211
+ } | undefined;
191
212
  engines: {
192
213
  [k: string]: string;
193
214
  } | undefined;
@@ -206,6 +227,7 @@ declare function getReadmeMetadata(): Promise<{
206
227
  projectDirectory: string | undefined;
207
228
  repositoryUrl: string | undefined;
208
229
  runtimePlatform: string[] | undefined;
230
+ usesGitLfs: boolean;
209
231
  usesPnpm: boolean;
210
232
  }>;
211
233
  /**
package/dist/lib/index.js CHANGED
@@ -31,7 +31,8 @@ function stripUndefinedDeep(object) {
31
31
  }, {});
32
32
  }
33
33
  function deepMergeDefined(...objects) {
34
- return deepmerge(...objects.map((v, i) => i === 0 ? v : stripUndefinedDeep(v)));
34
+ const stripped = objects.map((v, i) => i === 0 ? v : stripUndefinedDeep(v));
35
+ return deepmerge(...stripped);
35
36
  }
36
37
  //#endregion
37
38
  //#region src/lib/log.ts
@@ -98,6 +99,7 @@ async function getContextMetadata() {
98
99
  "codemetaJson",
99
100
  "gitConfig",
100
101
  "githubActions",
102
+ "gitStats",
101
103
  "goGoMod",
102
104
  "goGoreleaserYaml",
103
105
  "javaPomXml",
@@ -127,6 +129,7 @@ async function getContextMetadata() {
127
129
  const GIT_PREFIX_REGEX = /^git\+/v;
128
130
  const GIT_SUFFIX_REGEX = /\.git$/v;
129
131
  const TRAILING_SLASH_REGEX = /\/$/v;
132
+ const PACKAGE_MANAGER_REGEX = /^(?<name>[^@]+)@(?<version>[^+]+)/v;
130
133
  /**
131
134
  * Reset cached context metadata. Call between tests or when the underlying
132
135
  * project files may have changed on disk.
@@ -137,7 +140,7 @@ function resetContextMetadata() {
137
140
  metascopeMetadata = void 0;
138
141
  }
139
142
  const readmeMetadataTemplate = defineTemplate((context) => {
140
- const { githubActions, licenseFile, metascope, nodePackageJson } = context;
143
+ const { githubActions, gitStats, licenseFile, metascope, nodePackageJson } = context;
141
144
  const codemeta = templates.codemetaJson(context, {});
142
145
  const nodePackage = helpers.firstOf(nodePackageJson)?.data;
143
146
  const licenseFileData = helpers.firstOf(licenseFile);
@@ -170,6 +173,27 @@ const readmeMetadataTemplate = defineTemplate((context) => {
170
173
  version
171
174
  }));
172
175
  })();
176
+ const developmentDependencies = (() => {
177
+ if (nodePackage === void 0) return;
178
+ const runtimes = helpers.ensureArray(nodePackage.devEngines?.runtime).map(({ name, version }) => ({
179
+ name,
180
+ version
181
+ }));
182
+ const packageManagers = helpers.ensureArray(nodePackage.devEngines?.packageManager).map(({ name, version }) => ({
183
+ name,
184
+ version
185
+ }));
186
+ const pinnedPackageManager = nodePackage.packageManager === void 0 ? void 0 : PACKAGE_MANAGER_REGEX.exec(nodePackage.packageManager)?.groups;
187
+ if (pinnedPackageManager?.name !== void 0 && pinnedPackageManager.version !== void 0 && packageManagers.every((entry) => entry.name !== pinnedPackageManager.name)) packageManagers.push({
188
+ name: pinnedPackageManager.name,
189
+ version: pinnedPackageManager.version
190
+ });
191
+ if (runtimes.length === 0 && packageManagers.length === 0) return;
192
+ return {
193
+ packageManagers: packageManagers.length > 0 ? packageManagers : void 0,
194
+ runtimes: runtimes.length > 0 ? runtimes : void 0
195
+ };
196
+ })();
173
197
  const firstAuthor = helpers.firstOf(helpers.ensureArray(codemeta.author));
174
198
  return {
175
199
  author: helpers.firstOf(helpers.mixedStringsToArray(helpers.toBasicNames(codemeta.author))),
@@ -177,6 +201,7 @@ const readmeMetadataTemplate = defineTemplate((context) => {
177
201
  bin,
178
202
  ciActionFileName: ciActionFilePath === void 0 ? void 0 : path.basename(ciActionFilePath),
179
203
  description: codemeta.description,
204
+ developmentDependencies,
180
205
  engines,
181
206
  isPublicNpmPackage: nodePackage?.private !== true && (nodePackage?.name.startsWith("@") && nodePackage.publishConfig?.access === "public" || true),
182
207
  issuesUrl: codemeta.issueTracker,
@@ -189,6 +214,7 @@ const readmeMetadataTemplate = defineTemplate((context) => {
189
214
  projectDirectory: metascope?.data.options.path === void 0 ? void 0 : `file://${metascope.data.options.path}`,
190
215
  repositoryUrl: repoUrl,
191
216
  runtimePlatform: codemeta.runtimePlatform,
217
+ usesGitLfs: helpers.firstOf(gitStats)?.data.hasLfs === true,
192
218
  usesPnpm: helpers.usesPnpm(nodePackageJson)
193
219
  };
194
220
  });
@@ -232,13 +258,14 @@ var badges_default = { badges: { async content(options) {
232
258
  npm: z.array(z.string()).optional()
233
259
  }).optional().parse(options);
234
260
  const metadata = await getReadmeMetadata();
235
- const { ciActionFileName, license, licenseUrl, name, repositoryUrl } = metadata;
261
+ const { ciActionFileName, license, licenseUrl, name, repositoryUrl, usesGitLfs } = metadata;
236
262
  const badges = [];
237
263
  if (validOptions?.npm === void 0) {
238
264
  if (metadata.isPublicNpmPackage) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://www.npmjs.com/package/${name})`);
239
265
  } else for (const packageName of validOptions.npm) badges.push(`[![NPM Package ${packageName}](https://img.shields.io/npm/v/${packageName}.svg)](https://www.npmjs.com/package/${packageName})`);
240
266
  if (license !== void 0 && licenseUrl !== void 0) badges.push(`[![License: ${license}](https://img.shields.io/badge/License-${license.replaceAll("-", "--")}-yellow.svg)](${licenseUrl})`);
241
267
  if (ciActionFileName !== void 0 && repositoryUrl !== void 0) badges.push(`[![CI](${repositoryUrl}/actions/workflows/${ciActionFileName}/badge.svg)](${repositoryUrl}/actions/workflows/${ciActionFileName})`);
268
+ if (usesGitLfs) badges.push("![Git LFS](https://img.shields.io/badge/Git%20LFS-enabled-F64935?logo=gitlfs&logoColor=white)");
242
269
  if (validOptions?.custom !== void 0) for (const [badgeName, { image, link }] of Object.entries(validOptions.custom)) badges.push(`[![${badgeName}](${image})](${link})`);
243
270
  return badges.join("\n");
244
271
  } } };
@@ -357,7 +384,7 @@ const PLATFORM_INFO = {
357
384
  },
358
385
  node: {
359
386
  display: "Node.js",
360
- url: "https://nodejs.org/"
387
+ url: "https://nodejs.org/en"
361
388
  },
362
389
  python: {
363
390
  display: "Python",
@@ -416,6 +443,56 @@ var description_default = { description: { async content() {
416
443
  return `**${description}**`;
417
444
  } } };
418
445
  //#endregion
446
+ //#region src/lib/readme/rules/development-dependencies.ts
447
+ const TOOL_INFO = {
448
+ bun: {
449
+ display: "Bun",
450
+ url: "https://bun.sh/"
451
+ },
452
+ deno: {
453
+ display: "Deno",
454
+ url: "https://deno.land/"
455
+ },
456
+ node: {
457
+ display: "Node.js",
458
+ url: "https://nodejs.org/en"
459
+ },
460
+ npm: {
461
+ display: "npm",
462
+ url: "https://www.npmjs.com/"
463
+ },
464
+ pnpm: {
465
+ display: "pnpm",
466
+ url: "https://pnpm.io/"
467
+ },
468
+ yarn: {
469
+ display: "Yarn",
470
+ url: "https://yarnpkg.com/"
471
+ }
472
+ };
473
+ var development_dependencies_default = { "development-dependencies": { async content() {
474
+ const { developmentDependencies } = await getReadmeMetadata();
475
+ if (developmentDependencies === void 0) return "";
476
+ const { packageManagers, runtimes } = developmentDependencies;
477
+ const items = [];
478
+ for (const { name, version } of [...runtimes ?? [], ...packageManagers ?? []]) {
479
+ const info = TOOL_INFO[name.toLowerCase()];
480
+ const display = info ? `[${info.display}](${info.url})` : name;
481
+ items.push(version === void 0 || version === "" ? `- ${display}` : `- ${display} ${version}`);
482
+ }
483
+ return [
484
+ "## Development Dependencies",
485
+ "",
486
+ ...items
487
+ ].join("\n");
488
+ } } };
489
+ //#endregion
490
+ //#region src/lib/readme/rules/dev-dependencies.ts
491
+ /**
492
+ * Simple alias for `development-dependencies`
493
+ */
494
+ var dev_dependencies_default = { "dev-dependencies": getSoleRule(development_dependencies_default) };
495
+ //#endregion
419
496
  //#region src/lib/readme/rules/license.ts
420
497
  var license_default = { license: { async content() {
421
498
  const { author, authorUrl, license, licenseFilePath } = await getReadmeMetadata();
@@ -492,11 +569,11 @@ const gzipCompressAsync = promisify(gzip);
492
569
  * @param bytes - Size in bytes
493
570
  * @param originalSize - Original file size for percentage calculation
494
571
  */
495
- function createSizeInfo(bytes, originalSize) {
572
+ function createSizeInfo(bytes, originalSize, precision) {
496
573
  const percent = originalSize === 0 ? 0 : (originalSize - bytes) / originalSize * 100;
497
574
  return {
498
575
  bytes,
499
- bytesPretty: prettyBytes(bytes, { maximumFractionDigits: 1 }),
576
+ bytesPretty: prettyBytes(bytes, { maximumFractionDigits: precision }),
500
577
  percent,
501
578
  percentPretty: `${Math.round(percent)}%`
502
579
  };
@@ -505,19 +582,20 @@ function createSizeInfo(bytes, originalSize) {
505
582
  * Analyzes a file's size and its compressed sizes using Brotli and Gzip
506
583
  *
507
584
  * @param filePath - Path to the file to analyze
585
+ * @param precision - Maximum number of decimal places in formatted byte sizes
508
586
  *
509
587
  * @returns Promise containing detailed size report
510
588
  * @throws {Error} If file cannot be read or compressed
511
589
  */
512
- async function createSizeReport(filePath) {
590
+ async function createSizeReport(filePath, precision = 1) {
513
591
  try {
514
592
  const fileContent = await fs.readFile(filePath);
515
593
  const originalSize = fileContent.length;
516
594
  const [brotliCompressed, gzipCompressed] = await Promise.all([brotliCompressAsync(fileContent), gzipCompressAsync(fileContent)]);
517
595
  return {
518
- brotli: createSizeInfo(brotliCompressed.length, originalSize),
519
- gzip: createSizeInfo(gzipCompressed.length, originalSize),
520
- original: createSizeInfo(originalSize, originalSize)
596
+ brotli: createSizeInfo(brotliCompressed.length, originalSize, precision),
597
+ gzip: createSizeInfo(gzipCompressed.length, originalSize, precision),
598
+ original: createSizeInfo(originalSize, originalSize, precision)
521
599
  };
522
600
  } catch (error) {
523
601
  throw new Error(`Failed to analyze file: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
@@ -552,12 +630,14 @@ const optionsSchema = z.union([z.object({
552
630
  file: fileSchema,
553
631
  gzip: z.boolean().optional().default(true),
554
632
  original: z.boolean().optional().default(true),
633
+ precision: z.number().int().min(0).max(100).optional().default(0),
555
634
  showPercentage: z.boolean().optional().default(false)
556
635
  }), z.object({
557
636
  brotli: z.boolean().optional().default(true),
558
637
  files: fileSchema,
559
638
  gzip: z.boolean().optional().default(true),
560
639
  original: z.boolean().optional().default(true),
640
+ precision: z.number().int().min(0).max(100).optional().default(0),
561
641
  showPercentage: z.boolean().optional().default(false)
562
642
  })]).transform((options) => {
563
643
  if ("file" in options) {
@@ -591,7 +671,7 @@ function formatMarkdownTable(reports, options) {
591
671
  var size_table_default = { "size-table": { async content(options) {
592
672
  const validOptions = optionsSchema.parse(options);
593
673
  return formatMarkdownTable(await Promise.all(validOptions.files.map(async (file) => {
594
- return [file, await createSizeReport(path.join(process.cwd(), file))];
674
+ return [file, await createSizeReport(path.join(process.cwd(), file), validOptions.precision)];
595
675
  })), {
596
676
  brotli: validOptions.brotli,
597
677
  gzip: validOptions.gzip,
@@ -603,16 +683,17 @@ var size_table_default = { "size-table": { async content(options) {
603
683
  //#region src/lib/readme/rules/table-of-contents.ts
604
684
  var table_of_contents_default = { "table-of-contents": {
605
685
  async content(options, { tree }) {
686
+ const validOptions = z.object({ depth: z.union([
687
+ z.literal(1),
688
+ z.literal(2),
689
+ z.literal(3),
690
+ z.literal(4),
691
+ z.literal(5),
692
+ z.literal(6)
693
+ ]).optional() }).optional().parse(options);
606
694
  const result = toc(tree, {
607
695
  heading: null,
608
- maxDepth: z.object({ depth: z.union([
609
- z.literal(1),
610
- z.literal(2),
611
- z.literal(3),
612
- z.literal(4),
613
- z.literal(5),
614
- z.literal(6)
615
- ]).optional() }).optional().parse(options)?.depth ?? 3,
696
+ maxDepth: validOptions?.depth ?? 3,
616
697
  tight: true
617
698
  });
618
699
  if (result.map === void 0) throw new Error("Could not generate table of contents");
@@ -640,6 +721,8 @@ var rules_default = {
640
721
  ...contributing_default,
641
722
  ...dependencies_default,
642
723
  ...description_default,
724
+ ...dev_dependencies_default,
725
+ ...development_dependencies_default,
643
726
  ...footer_default,
644
727
  ...header_default,
645
728
  ...install_default,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdat",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "CLI tool and TypeScript library implementing the Markdown Autophagic Template (MDAT) system. MDAT lets you use comments as dynamic content templates in Markdown files, making it easy to generate and update readme boilerplate.",
5
5
  "keywords": [
6
6
  "mdat",
@@ -44,50 +44,50 @@
44
44
  ],
45
45
  "dependencies": {
46
46
  "@clack/prompts": "^1.7.0",
47
- "cosmiconfig": "^9.0.2",
47
+ "cosmiconfig": "^10.0.1",
48
48
  "cosmiconfig-typescript-loader": "^6.3.0",
49
- "deepmerge-ts": "^7.1.5",
50
- "globby": "^16.2.2",
51
- "lognow": "^0.7.1",
49
+ "deepmerge-ts": "^8.0.2",
50
+ "globby": "^16.2.4",
51
+ "lognow": "^0.8.0",
52
52
  "mdast-util-toc": "^7.1.0",
53
- "metascope": "^0.9.0",
53
+ "metascope": "^0.11.1",
54
54
  "path-type": "^6.0.0",
55
55
  "picocolors": "^1.1.1",
56
56
  "plur": "^6.0.0",
57
- "pretty-bytes": "^7.1.1",
58
- "pretty-ms": "^9.3.0",
57
+ "pretty-bytes": "^7.1.2",
58
+ "pretty-ms": "^9.3.1",
59
59
  "remark": "^15.0.1",
60
60
  "remark-gfm": "^4.0.1",
61
- "remark-mdat": "^3.0.0",
61
+ "remark-mdat": "^3.0.1",
62
62
  "to-vfile": "^8.0.0",
63
- "type-fest": "^5.8.0",
63
+ "type-fest": "^5.9.0",
64
64
  "unified-engine": "^11.2.2",
65
65
  "untildify": "^6.0.0",
66
66
  "vfile": "^6.0.3",
67
- "yargs": "^18.0.0",
68
- "zod": "^4.4.3"
67
+ "yargs": "^18.1.0",
68
+ "zod": "^4.5.4"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@arethetypeswrong/core": "^0.18.5",
72
- "@kitschpatrol/shared-config": "^8.4.0",
72
+ "@kitschpatrol/shared-config": "^8.7.0",
73
73
  "@types/mdast": "^4.0.4",
74
74
  "@types/node": "~24.13.3",
75
75
  "@types/unist": "^3.0.3",
76
76
  "@types/yargs": "^17.0.35",
77
- "@vitest/coverage-v8": "^4.1.10",
78
- "bumpp": "^12.0.0",
79
- "execa": "^10.0.0",
80
- "mdat-plugin-cli-help": "^3.0.2",
81
- "mdat-plugin-example": "^2.0.1",
82
- "mdat-plugin-tldraw": "^2.0.3",
83
- "nanoid": "^6.0.0",
77
+ "@vitest/coverage-v8": "^4.1.11",
78
+ "bumpp": "^12.2.2",
79
+ "execa": "^10.0.1",
80
+ "mdat-plugin-cli-help": "^3.2.0",
81
+ "mdat-plugin-example": "^3.0.0",
82
+ "mdat-plugin-tldraw": "^3.0.0",
83
+ "nanoid": "^6.0.1",
84
84
  "prettier": "^3.9.6",
85
- "publint": "^0.3.22",
85
+ "publint": "^0.3.24",
86
86
  "shx": "^0.4.0",
87
87
  "tsdown": "^0.22.14",
88
88
  "typescript": "~6.0.3",
89
- "unplugin-raw": "^0.7.0",
90
- "vitest": "^4.1.10"
89
+ "unplugin-raw": "^0.8.0",
90
+ "vitest": "^4.1.11"
91
91
  },
92
92
  "peerDependencies": {
93
93
  "prettier": "^3.0.0"
@@ -108,7 +108,7 @@
108
108
  "coverage": "vitest --coverage --run",
109
109
  "fix": "ksc fix",
110
110
  "lint": "ksc lint",
111
- "release": "bumpp --commit 'Release: %s' && pnpm build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') pnpm publish",
111
+ "release": "bumpp --commit 'Release: %s' && pnpm build && pnpm publish --otp $(op item get npm --otp)",
112
112
  "test": "vitest run"
113
113
  }
114
114
  }
package/readme.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  <!-- badges -->
8
8
 
9
- [![NPM Package mdat](https://img.shields.io/npm/v/mdat.svg)](https://npmjs.com/package/mdat)
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/mit/)
9
+ [![NPM Package mdat](https://img.shields.io/npm/v/mdat.svg)](https://www.npmjs.com/package/mdat)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/mit)
11
11
  [![CI](https://github.com/kitschpatrol/mdat/actions/workflows/ci.yml/badge.svg)](https://github.com/kitschpatrol/mdat/actions/workflows/ci.yml)
12
12
 
13
13
  <!-- /badges -->
@@ -176,7 +176,7 @@ pnpm install --global mdat
176
176
  }
177
177
  ```
178
178
 
179
- See the [bundled rules](src/lib/readme/rules) for more complex examples.
179
+ See the [bundled rules](./src/lib/readme/rules) for more complex examples.
180
180
 
181
181
  JSON files can also be used as rule sets. MDAT flattens them so any dot-notated key path becomes a comment keyword.
182
182
 
@@ -192,7 +192,7 @@ pnpm install --global mdat
192
192
 
193
193
  Compound rules combine several individual rules into a single comment keyword.
194
194
 
195
- See [`<!-- header -->`](src/lib/readme/rules/header.ts) for an example.
195
+ See [`<!-- header -->`](./src/lib/readme/rules/header.ts) for an example.
196
196
 
197
197
  8. **Polyglot metadata**
198
198
 
@@ -548,7 +548,7 @@ When multiple config files are loaded, they are merged. CLI `--config` takes pre
548
548
 
549
549
  ### Creating custom rules
550
550
 
551
- See the [Examples section](https://github.com/kitschpatrol/remark-mdat#examples) of the `remark-mdat` readme, or look at the [bundled rules](src/lib/readme/rules) for complex examples.
551
+ See the [Examples section](https://github.com/kitschpatrol/remark-mdat#examples) of the `remark-mdat` readme, or look at the [bundled rules](./src/lib/readme/rules) for complex examples.
552
552
 
553
553
  ### Bundled rules
554
554
 
@@ -564,7 +564,7 @@ See the [Examples section](https://github.com/kitschpatrol/remark-mdat#examples)
564
564
 
565
565
  - ##### `<!-- badges -->`
566
566
 
567
- Generates badges based on project metadata. Supports NPM version, license, and CI status badges.
567
+ Generates badges based on project metadata. Supports NPM version, license, CI status, and Git LFS badges.
568
568
 
569
569
  - ##### `<!-- description -->`
570
570
 
@@ -578,6 +578,10 @@ See the [Examples section](https://github.com/kitschpatrol/remark-mdat#examples)
578
578
 
579
579
  Documents platform requirements and peer dependencies. Lists runtime platforms (Node, Python, Rust, Go, Ruby, etc.) with version constraints from `engines` or equivalent metadata, supported operating systems, and peer dependencies with links to npm.
580
580
 
581
+ - ##### `<!-- development-dependencies -->`
582
+
583
+ Documents the tools needed to work on the project itself, derived from the `devEngines` and `packageManager` fields in `package.json`. Lists required runtimes and package managers with links and version constraints. Also aliased as `<!-- dev-dependencies -->`.
584
+
581
585
  - ##### `<!-- table-of-contents -->`
582
586
 
583
587
  Auto-generated via [mdast-util-toc](https://github.com/syntax-tree/mdast-util-toc). Also aliased as `<!-- toc -->`.
@@ -600,7 +604,7 @@ See the [Examples section](https://github.com/kitschpatrol/remark-mdat#examples)
600
604
 
601
605
  - ##### `<!-- size-table({ files: [".gitignore", "license.txt"] }) -->`
602
606
 
603
- A table of files and their compressed sizes:
607
+ A table of files and their compressed sizes. The optional `precision` sets the maximum number of decimal places and defaults to `0`:
604
608
 
605
609
  <!-- size-table({ files: [".gitignore", "license.txt"] }) -->
606
610
 
@@ -792,7 +796,7 @@ There's quite a bit of prior art and similar explorations of this problem space:
792
796
  - lillallol's [md-in-place](https://www.npmjs.com/package/md-in-place)
793
797
 
794
798
  - [AutoMD](https://automd.unjs.io/)\
795
- Extremely similar functionality to mdat. The project was initiated around the same time as MDAT, but I didn't find the project until a few years later. Ships in the night.
799
+ Extremely similar functionality to mdat. The project was initiated around the same time as MDAT, but I didn't find the project until a few years later. Ships passing in the night.
796
800
 
797
801
  - Franck Abgrall's [readme-md-generator](https://github.com/kefranabg/readme-md-generator)
798
802
 
@@ -802,7 +806,8 @@ There's quite a bit of prior art and similar explorations of this problem space:
802
806
 
803
807
  - Hiroki Osame's [comment-mark](https://github.com/privatenumber/comment-mark)
804
808
 
805
- - Hiroki Osame's [mdeval](https://github.com/privatenumber/mdeval)
809
+ - Hiroki Osame's [mdeval](https://github.com/privatenumber/mdeval)\
810
+ The best name!
806
811
 
807
812
  ### Implementation notes
808
813