mdat 2.0.0 → 2.0.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/dist/.DS_Store CHANGED
Binary file
package/dist/bin/cli.js CHANGED
@@ -7,9 +7,9 @@ import { TypeScriptLoader } from "cosmiconfig-typescript-loader";
7
7
  import fs from "node:fs/promises";
8
8
  import path from "node:path";
9
9
  import plur from "plur";
10
- import { defineTemplate, getMetadata, helpers, setLogger as setLogger$1, templates } from "metascope";
11
10
  import { deepmerge } from "deepmerge-ts";
12
11
  import { createLogger, getChildLogger, injectionHelper } from "lognow";
12
+ import { defineTemplate, getMetadata, helpers, setLogger as setLogger$1, templates } from "metascope";
13
13
  import { z } from "zod";
14
14
  import { globby } from "globby";
15
15
  import { isFile, isFileSync } from "path-type";
@@ -27,65 +27,6 @@ import { confirm, group, intro, note, outro, select } from "@clack/prompts";
27
27
  import yargs from "yargs";
28
28
  import { hideBin } from "yargs/helpers";
29
29
 
30
- //#region src/lib/context.ts
31
- let metascopeMetadata;
32
- /**
33
- * Get a bunch of platform-agnostic local metadata via metascope, exposed
34
- * primarily for plugin developers.
35
- * Result is memoized the result.
36
- * @throws {Error} If no package.json is found
37
- */
38
- async function getContextMetadata() {
39
- if (metascopeMetadata !== void 0) return metascopeMetadata;
40
- metascopeMetadata = await getMetadata({
41
- absolute: false,
42
- offline: true
43
- });
44
- return metascopeMetadata;
45
- }
46
- /**
47
- * Reset
48
- */
49
- function resetContextMetadata() {
50
- metascopeMetadata = void 0;
51
- }
52
- const readmeMetadataTemplate = defineTemplate((context) => {
53
- const { githubActions, licenseFile, metascope, nodePackageJson } = context;
54
- const codemeta = templates.codemetaJson(context, {});
55
- const nodePackage = helpers.firstOf(nodePackageJson)?.data;
56
- const licenseFileData = helpers.firstOf(licenseFile);
57
- const ciActionFilePath = helpers.ensureArray(githubActions).find((entry) => entry.data.name.toLowerCase() === "ci")?.source;
58
- const repositoryOwner = codemeta.codeRepository ? new URL(codemeta.codeRepository).pathname.split("/")[1] : void 0;
59
- return {
60
- author: helpers.firstOf(helpers.mixedStringsToArray(helpers.toBasicNames(codemeta.author))),
61
- ciActionFileName: ciActionFilePath ? path.basename(ciActionFilePath) : void 0,
62
- description: codemeta.description,
63
- isPublicNpmPackage: !nodePackage?.name.startsWith("@") || nodePackage.publishConfig?.access === "public",
64
- issuesUrl: codemeta.issueTracker,
65
- license: helpers.toBasicLicense(helpers.firstOf(helpers.ensureArray(codemeta.license))),
66
- licenseFilePath: licenseFileData?.source,
67
- name: codemeta.name,
68
- projectDirectory: metascope?.data.options.path === void 0 ? void 0 : `file://${metascope.data.options.path}`,
69
- repositoryOwner
70
- };
71
- });
72
- let readmeMetadata;
73
- /**
74
- * Nice data for readme rules
75
- */
76
- async function getReadmeMetadata() {
77
- if (readmeMetadata !== void 0) return readmeMetadata;
78
- readmeMetadata = readmeMetadataTemplate(await getContextMetadata(), {});
79
- return readmeMetadata;
80
- }
81
- /**
82
- * Reset
83
- */
84
- function resetReadmeMetadata() {
85
- readmeMetadata = void 0;
86
- }
87
-
88
- //#endregion
89
30
  //#region src/lib/deep-merge-defined.ts
90
31
  function stripUndefinedDeep(object) {
91
32
  if (Array.isArray(object)) return object.map((v) => v && typeof v === "object" ? stripUndefinedDeep(v) : v).filter((v) => v !== void 0);
@@ -101,7 +42,7 @@ function deepMergeDefined(...objects) {
101
42
  //#endregion
102
43
  //#region package.json
103
44
  var name = "mdat";
104
- var version = "2.0.0";
45
+ var version = "2.0.1";
105
46
 
106
47
  //#endregion
107
48
  //#region src/lib/log.ts
@@ -146,6 +87,87 @@ function flattenJson(jsonObject, parentKey = "", result = {}) {
146
87
  return result;
147
88
  }
148
89
 
90
+ //#endregion
91
+ //#region src/lib/context.ts
92
+ let metascopeMetadata;
93
+ /**
94
+ * Get a bunch of platform-agnostic local metadata via metascope, exposed
95
+ * primarily for plugin developers. Result is memoized the result.
96
+ *
97
+ * @throws {Error} If no package.json is found
98
+ */
99
+ async function getContextMetadata() {
100
+ if (metascopeMetadata !== void 0) return metascopeMetadata;
101
+ metascopeMetadata = await getMetadata({
102
+ absolute: false,
103
+ offline: true,
104
+ sources: [
105
+ "arduinoLibraryProperties",
106
+ "cinderCinderblockXml",
107
+ "codemetaJson",
108
+ "gitConfig",
109
+ "githubActions",
110
+ "goGoMod",
111
+ "goGoreleaserYaml",
112
+ "javaPomXml",
113
+ "licenseFile",
114
+ "metadataFile",
115
+ "metascope",
116
+ "nodePackageJson",
117
+ "obsidianPluginManifestJson",
118
+ "openframeworksAddonConfigMk",
119
+ "openframeworksInstallXml",
120
+ "processingLibraryProperties",
121
+ "processingSketchProperties",
122
+ "publiccodeYaml",
123
+ "pythonPkgInfo",
124
+ "pythonPyprojectToml",
125
+ "pythonSetupCfg",
126
+ "pythonSetupPy",
127
+ "readmeFile",
128
+ "rubyGemspec",
129
+ "rustCargoToml",
130
+ "xcodeInfoPlist",
131
+ "xcodeProjectPbxproj"
132
+ ]
133
+ });
134
+ return metascopeMetadata;
135
+ }
136
+ const GIT_PREFIX_REGEX = /^git\+/;
137
+ const GIT_SUFFIX_REGEX = /\.git$/;
138
+ const TRAILING_SLASH_REGEX = /\/$/;
139
+ const readmeMetadataTemplate = defineTemplate((context) => {
140
+ const { githubActions, licenseFile, metascope, nodePackageJson } = context;
141
+ const codemeta = templates.codemetaJson(context, {});
142
+ const nodePackage = helpers.firstOf(nodePackageJson)?.data;
143
+ const licenseFileData = helpers.firstOf(licenseFile);
144
+ const ciActionFilePath = helpers.ensureArray(githubActions).find((entry) => entry.data.name.toLowerCase() === "ci")?.source;
145
+ const repositoryUrl = codemeta.codeRepository?.replace(GIT_PREFIX_REGEX, "").replace(GIT_SUFFIX_REGEX, "").replace(TRAILING_SLASH_REGEX, "");
146
+ return {
147
+ author: helpers.firstOf(helpers.mixedStringsToArray(helpers.toBasicNames(codemeta.author))),
148
+ ciActionFileName: ciActionFilePath ? path.basename(ciActionFilePath) : void 0,
149
+ description: codemeta.description,
150
+ isPublicNpmPackage: !nodePackage?.name.startsWith("@") || nodePackage.publishConfig?.access === "public",
151
+ issuesUrl: codemeta.issueTracker,
152
+ license: helpers.toBasicLicense(helpers.firstOf(helpers.ensureArray(codemeta.license))),
153
+ licenseFilePath: licenseFileData?.source,
154
+ name: codemeta.name,
155
+ projectDirectory: metascope?.data.options.path === void 0 ? void 0 : `file://${metascope.data.options.path}`,
156
+ repositoryUrl
157
+ };
158
+ });
159
+ let readmeMetadata;
160
+ /**
161
+ * Nice data for readme rules
162
+ *
163
+ * @public
164
+ */
165
+ async function getReadmeMetadata() {
166
+ if (readmeMetadata !== void 0) return readmeMetadata;
167
+ readmeMetadata = readmeMetadataTemplate(await getContextMetadata(), {});
168
+ return readmeMetadata;
169
+ }
170
+
149
171
  //#endregion
150
172
  //#region src/lib/readme/rules/badges.ts
151
173
  var badges_default = { badges: { async content(options) {
@@ -157,13 +179,13 @@ var badges_default = { badges: { async content(options) {
157
179
  npm: z.array(z.string()).optional()
158
180
  }).optional().parse(options);
159
181
  const metadata = await getReadmeMetadata();
160
- const { ciActionFileName, license, name, repositoryOwner } = metadata;
182
+ const { ciActionFileName, license, name, repositoryUrl } = metadata;
161
183
  const badges = [];
162
184
  if (validOptions?.npm === void 0) {
163
185
  if (metadata.isPublicNpmPackage) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://npmjs.com/package/${name})`);
164
186
  } else for (const name of validOptions.npm) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://npmjs.com/package/${name})`);
165
187
  if (license !== void 0) badges.push(`[![License: ${license}](https://img.shields.io/badge/License-${license.replaceAll("-", "--")}-yellow.svg)](https://opensource.org/licenses/${license})`);
166
- if (ciActionFileName !== void 0 && repositoryOwner !== void 0) badges.push(`[![CI](https://github.com/${repositoryOwner}/${name}/actions/workflows/${ciActionFileName}/badge.svg)](https://github.com/${repositoryOwner}/${name}/actions/workflows/${ciActionFileName})`);
188
+ if (ciActionFileName !== void 0 && repositoryUrl !== void 0) badges.push(`[![CI](${repositoryUrl}/actions/workflows/${ciActionFileName}/badge.svg)](${repositoryUrl}/actions/workflows/${ciActionFileName})`);
167
189
  if (validOptions?.custom !== void 0) for (const [name, { image, link }] of Object.entries(validOptions.custom)) badges.push(`[![${name}](${image})](${link})`);
168
190
  return badges.join("\n");
169
191
  } } };
@@ -231,24 +253,21 @@ function isUrl(text, lenient = true) {
231
253
  if (typeof text !== "string") throw new TypeError("Expected a string");
232
254
  text = text.trim();
233
255
  if (text.includes(" ")) return false;
234
- try {
235
- new URL(text);
236
- return true;
237
- } catch {
238
- if (lenient) return isUrl(`https://${text}`, false);
239
- return false;
240
- }
256
+ if (URL.canParse(text)) return true;
257
+ if (lenient) return isUrl(`https://${text}`, false);
258
+ return false;
241
259
  }
242
260
 
243
261
  //#endregion
244
262
  //#region src/lib/readme/rules/code.ts
263
+ const LEADING_DOT_REGEX = /^\./;
245
264
  var code_default = { code: { async content(options) {
246
265
  const validOptions = z.object({
247
266
  file: z.string(),
248
267
  language: z.string().optional(),
249
268
  trim: z.boolean().default(true)
250
269
  }).parse(options);
251
- const lang = (path.extname(validOptions.file) ?? "").replace(/^\./, "");
270
+ const lang = (path.extname(validOptions.file) ?? "").replace(LEADING_DOT_REGEX, "");
252
271
  const exampleCode = await fs.readFile(path.join(process.cwd(), validOptions.file), "utf8");
253
272
  return `\`\`\`${lang}\n${validOptions.trim ? exampleCode.trim() : exampleCode}\n\`\`\``;
254
273
  } } };
@@ -309,8 +328,9 @@ var title_default = { title: {
309
328
  },
310
329
  order: 2
311
330
  } };
331
+ const SPLIT_FOR_TITLE_CASE_REGEX = /[ _-]+/;
312
332
  function makeTitleCase(text) {
313
- return text.split(/[ _-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
333
+ return text.split(SPLIT_FOR_TITLE_CASE_REGEX).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
314
334
  }
315
335
 
316
336
  //#endregion
@@ -497,6 +517,19 @@ var rules_default = {
497
517
 
498
518
  //#endregion
499
519
  //#region src/lib/config.ts
520
+ let _configExplorer;
521
+ let _additionalConfigExplorer;
522
+ function getConfigExplorer() {
523
+ _configExplorer ??= cosmiconfig("mdat", { loaders: { ".ts": TypeScriptLoader() } });
524
+ return _configExplorer;
525
+ }
526
+ function getAdditionalConfigExplorer() {
527
+ _additionalConfigExplorer ??= cosmiconfig("mdat", { loaders: {
528
+ ".json": mdatJsonLoader,
529
+ ".ts": TypeScriptLoader()
530
+ } });
531
+ return _additionalConfigExplorer;
532
+ }
500
533
  /**
501
534
  * Load and validate mdat configuration.
502
535
  * Uses cosmiconfig to search in the usual places.
@@ -504,11 +537,9 @@ var rules_default = {
504
537
  */
505
538
  async function loadConfig(options) {
506
539
  const { additionalConfig, defaults = rules_default, searchFrom } = options ?? {};
507
- resetReadmeMetadata();
508
- resetContextMetadata();
509
540
  let finalConfig = { mdat: `Powered by the Markdown Autophagic Template system: [mdat](https://github.com/kitschpatrol/mdat).` };
510
541
  if (defaults) finalConfig = deepMergeDefined(finalConfig, defaults);
511
- const results = await cosmiconfig("mdat", { loaders: { ".ts": TypeScriptLoader() } }).search(searchFrom);
542
+ const results = await getConfigExplorer().search(searchFrom);
512
543
  if (results) {
513
544
  const { config, filepath } = results;
514
545
  let possibleRules = config;
@@ -523,10 +554,6 @@ async function loadConfig(options) {
523
554
  }
524
555
  if (additionalConfig !== void 0) {
525
556
  const additionalConfigArray = Array.isArray(additionalConfig) ? additionalConfig : [additionalConfig];
526
- const configExplorer2 = cosmiconfig("mdat", { loaders: {
527
- ".json": mdatJsonLoader,
528
- ".ts": TypeScriptLoader()
529
- } });
530
557
  for (const configOrPath of additionalConfigArray) {
531
558
  let loaded;
532
559
  if (typeof configOrPath === "string") {
@@ -535,7 +562,7 @@ async function loadConfig(options) {
535
562
  config: mdatJsonLoader(configOrPath, await fs.readFile(configOrPath, "utf8")),
536
563
  filepath: configOrPath
537
564
  };
538
- else results = await configExplorer2.load(configOrPath);
565
+ else results = await getAdditionalConfigExplorer().load(configOrPath);
539
566
  if (results === null || results === void 0) continue;
540
567
  const { config: loadedConfig, filepath } = results;
541
568
  log.debug(`Loaded additional config from "${filepath}"`);
@@ -559,20 +586,26 @@ function validateConfig(value) {
559
586
 
560
587
  //#endregion
561
588
  //#region src/lib/format.ts
589
+ let cachedPrettier;
590
+ const configCache = /* @__PURE__ */ new Map();
562
591
  /**
563
592
  * Format a markdown string with Prettier, using config discovered from the file path.
564
593
  * Requires `prettier` to be installed as a peer dependency.
565
594
  */
566
595
  async function formatWithPrettier(content, filePath) {
567
- let prettier;
568
- try {
569
- prettier = await import("prettier");
596
+ if (cachedPrettier === void 0) try {
597
+ cachedPrettier = await import("prettier");
570
598
  } catch {
571
599
  throw new Error("The --format flag requires `prettier` to be installed. Run: pnpm add -D prettier");
572
600
  }
573
- const config = await prettier.resolveConfig(filePath ?? process.cwd());
574
- if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
575
- return prettier.format(content, {
601
+ const configKey = filePath ? path.dirname(filePath) : process.cwd();
602
+ let config = configCache.get(configKey);
603
+ if (config === void 0 && !configCache.has(configKey)) {
604
+ config = await cachedPrettier.resolveConfig(filePath ?? process.cwd());
605
+ configCache.set(configKey, config);
606
+ if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
607
+ }
608
+ return cachedPrettier.format(content, {
576
609
  ...config,
577
610
  filepath: filePath,
578
611
  parser: "markdown"
@@ -614,12 +647,13 @@ function ensureArray(value) {
614
647
  if (value === void 0 || value === null) return [];
615
648
  return Array.isArray(value) ? value : [value];
616
649
  }
650
+ const README_SEARCH_REGEX = /^readme(?:\.\w+)?$/i;
617
651
  /**
618
652
  * Finds a readme file in the current working directory (case-insensitive).
619
653
  */
620
654
  async function findReadme() {
621
655
  log.debug("Searching for readme in current directory...");
622
- const readme = (await fs.readdir(process.cwd())).find((entry) => /^readme(?:\.\w+)?$/i.test(entry));
656
+ const readme = (await fs.readdir(process.cwd())).find((entry) => README_SEARCH_REGEX.test(entry));
623
657
  if (readme !== void 0) {
624
658
  const absolutePath = path.resolve(readme);
625
659
  log.debug(`Found readme at "${absolutePath}"`);
@@ -635,7 +669,9 @@ async function findReadmeThrows() {
635
669
  if (readme === void 0) throw new Error("No readme found");
636
670
  return readme;
637
671
  }
672
+ let cachedAmbientRemarkConfig;
638
673
  async function loadAmbientRemarkConfig() {
674
+ if (cachedAmbientRemarkConfig !== void 0) return cachedAmbientRemarkConfig;
639
675
  const ambientConfig = new Configuration({
640
676
  cwd: process.cwd(),
641
677
  detectConfig: true,
@@ -656,32 +692,47 @@ async function loadAmbientRemarkConfig() {
656
692
  const { filePath } = configResult;
657
693
  if (filePath === void 0) log.debug("No ambient Remark configuration file found");
658
694
  else log.debug(`Found and loaded ambient Remark configuration from "${filePath}"`);
659
- return configResult;
695
+ cachedAmbientRemarkConfig = stripLintPlugins(configResult);
696
+ return cachedAmbientRemarkConfig;
660
697
  }
661
698
  log.debug("No ambient Remark configuration found");
662
- return {
699
+ cachedAmbientRemarkConfig = {
663
700
  filePath: void 0,
664
701
  plugins: [],
665
702
  settings: {}
666
703
  };
704
+ return cachedAmbientRemarkConfig;
705
+ }
706
+ /**
707
+ * Strip remark-lint plugins from an ambient config. Lint plugins only produce
708
+ * VFile warnings and never modify the AST or output — running them during
709
+ * expansion is pure overhead.
710
+ */
711
+ function stripLintPlugins(config) {
712
+ return {
713
+ ...config,
714
+ plugins: config.plugins.filter((entry) => {
715
+ const plugin = Array.isArray(entry) ? entry[0] : entry;
716
+ if (typeof plugin !== "function") return true;
717
+ const { name } = plugin;
718
+ return name !== "remarkLint" && !name.startsWith("remark-lint:") && name !== "remarkValidateLinks";
719
+ })
720
+ };
667
721
  }
668
722
 
669
723
  //#endregion
670
724
  //#region src/lib/processors.ts
671
725
  async function processFiles(files, loader, processorGetter, name, output, config) {
672
- const resolvedConfig = await loader({ additionalConfig: config });
673
- const localRemarkConfiguration = await loadAmbientRemarkConfig();
726
+ const [resolvedConfig, localRemarkConfiguration] = await Promise.all([loader({ additionalConfig: config }), loadAmbientRemarkConfig()]);
674
727
  const inputOutputPaths = await getInputOutputPaths(ensureArray(files), output, name, "md");
675
- const results = [];
676
728
  const resolvedProcessor = processorGetter(resolvedConfig, localRemarkConfiguration);
677
- for (const { input, name, output } of inputOutputPaths) {
729
+ return await Promise.all(inputOutputPaths.map(async ({ input, name, output }) => {
678
730
  const inputFile = await read(input);
679
731
  const result = await resolvedProcessor.process(inputFile);
680
732
  result.dirname = output;
681
733
  result.basename = name;
682
- results.push(result);
683
- }
684
- return results;
734
+ return result;
735
+ }));
685
736
  }
686
737
  function getExpandProcessor(config, ambientRemarkConfig) {
687
738
  return remark().use({ settings: {
@@ -870,11 +921,10 @@ async function check(files, config, options) {
870
921
  files ??= await findReadmeThrows();
871
922
  const { read } = await import("to-vfile");
872
923
  const resolvedFiles = Array.isArray(files) ? files : [files];
873
- const originals = await Promise.all(resolvedFiles.map(async (f) => read(f)));
874
- const results = await processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config);
924
+ const [originals, results] = await Promise.all([Promise.all(resolvedFiles.map(async (f) => read(f))), processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config)]);
875
925
  if (options?.format) await formatResults(results);
876
926
  return results.map((result, i) => ({
877
- inSync: originals[i].toString() === result.toString(),
927
+ inSync: originals[i].toString().replaceAll("\r\n", "\n") === result.toString(),
878
928
  result
879
929
  }));
880
930
  }
@@ -961,7 +1011,11 @@ try {
961
1011
  setLogger$2(createLogger({
962
1012
  name,
963
1013
  verbose: argv.verbose ?? false,
964
- logToConsole: { showTime: false }
1014
+ logToConsole: {
1015
+ showLevel: false,
1016
+ showName: false,
1017
+ showTime: false
1018
+ }
965
1019
  }));
966
1020
  }).command(["$0 [files..] [options]", "expand [files..] [options]"], "Expand MDAT placeholder comments. If no files are provided, the closest readme.md is expanded.", (yargs) => yargs.positional(...filesPositional).option(configOption).option(outputOption).option(nameOption).option(printOption).option(formatOption), async ({ config, files, format, name, output, print }) => {
967
1021
  logConflicts({
@@ -987,14 +1041,14 @@ try {
987
1041
  reporterMdat(results);
988
1042
  log.debug(`Collapsed comments in ${prettyMilliseconds(performance.now() - startTime)}.`);
989
1043
  process.exitCode = getExitCode(results);
990
- }).command("check [files..] [options]", "Check if MDAT placeholder comments are up to date. Exits with code 1 if any files are out of sync.", (yargs) => yargs.positional(...filesPositional).option(configOption).option(formatOption), async ({ config, files, format }) => {
1044
+ }).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.", (yargs) => yargs.positional(...filesPositional).option(configOption).option(formatOption), async ({ config, files, format }) => {
991
1045
  const results = await check(files, collectConfig(config), { format });
992
1046
  let allInSync = true;
993
1047
  for (const { inSync, result } of results) {
994
1048
  const filePath = result.path || "unknown";
995
- if (inSync) log.info(`${picocolors.green("in sync")}: ${filePath}`);
1049
+ if (inSync) log.debug(`${picocolors.green("Up to date")}: ${filePath}`);
996
1050
  else {
997
- log.info(`${picocolors.red("out of sync")}: ${filePath}`);
1051
+ log.warn(`${picocolors.red("Stale content")}: ${filePath}`);
998
1052
  allInSync = false;
999
1053
  }
1000
1054
  }
@@ -1,7 +1,7 @@
1
1
  import { Rule, Rule as Rule$1 } from "remark-mdat";
2
+ import { ILogBasic, ILogLayer } from "lognow";
2
3
  import * as _$metascope from "metascope";
3
4
  import { MetadataContext } from "metascope";
4
- import { ILogBasic, ILogLayer } from "lognow";
5
5
  import { VFile } from "vfile";
6
6
 
7
7
  //#region src/lib/config.d.ts
@@ -111,8 +111,8 @@ declare function check(files?: string | string[], config?: ConfigToLoad, options
111
111
  //#region src/lib/context.d.ts
112
112
  /**
113
113
  * Get a bunch of platform-agnostic local metadata via metascope, exposed
114
- * primarily for plugin developers.
115
- * Result is memoized the result.
114
+ * primarily for plugin developers. Result is memoized the result.
115
+ *
116
116
  * @throws {Error} If no package.json is found
117
117
  */
118
118
  declare function getContextMetadata(): Promise<MetadataContext>;
@@ -126,11 +126,13 @@ declare const readmeMetadataTemplate: _$metascope.Template<{
126
126
  licenseFilePath: string | undefined;
127
127
  name: string | undefined;
128
128
  projectDirectory: string | undefined;
129
- repositoryOwner: string | undefined;
129
+ repositoryUrl: string | undefined;
130
130
  }>;
131
131
  type ReadmeMetadata = ReturnType<typeof readmeMetadataTemplate>;
132
132
  /**
133
133
  * Nice data for readme rules
134
+ *
135
+ * @public
134
136
  */
135
137
  declare function getReadmeMetadata(): Promise<{
136
138
  author: string | undefined;
@@ -142,8 +144,13 @@ declare function getReadmeMetadata(): Promise<{
142
144
  licenseFilePath: string | undefined;
143
145
  name: string | undefined;
144
146
  projectDirectory: string | undefined;
145
- repositoryOwner: string | undefined;
147
+ repositoryUrl: string | undefined;
146
148
  }>;
149
+ /**
150
+ * Reset all cached metadata. Call between tests or when the underlying project
151
+ * files may have changed on disk.
152
+ */
153
+ declare function resetMetadataCaches(): void;
147
154
  //#endregion
148
155
  //#region src/lib/log.d.ts
149
156
  /**
@@ -153,4 +160,4 @@ declare function getReadmeMetadata(): Promise<{
153
160
  */
154
161
  declare function setLogger(logger?: ILogBasic | ILogLayer): void;
155
162
  //#endregion
156
- export { type Config, type ReadmeMetadata, type Rule, check, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, setLogger };
163
+ export { type Config, type ReadmeMetadata, type Rule, check, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger };
package/dist/lib/index.js CHANGED
@@ -5,9 +5,9 @@ import path from "node:path";
5
5
  import picocolors from "picocolors";
6
6
  import plur from "plur";
7
7
  import { getSoleRule, mdatClean, mdatExpand, mdatSplit, rulesSchema, setLogger as setLogger$1 } from "remark-mdat";
8
- import { defineTemplate, getMetadata, helpers, setLogger as setLogger$2, templates } from "metascope";
9
8
  import { deepmerge } from "deepmerge-ts";
10
9
  import { createLogger, getChildLogger, injectionHelper } from "lognow";
10
+ import { defineTemplate, getMetadata, helpers, setLogger as setLogger$2, templates } from "metascope";
11
11
  import { z } from "zod";
12
12
  import { globby } from "globby";
13
13
  import { isFile, isFileSync } from "path-type";
@@ -22,24 +22,111 @@ import { VFile } from "vfile";
22
22
  import { Configuration } from "unified-engine";
23
23
  import untildify from "untildify";
24
24
  import { confirm, group, intro, note, outro, select } from "@clack/prompts";
25
+ //#region src/lib/deep-merge-defined.ts
26
+ function stripUndefinedDeep(object) {
27
+ if (Array.isArray(object)) return object.map((v) => v && typeof v === "object" ? stripUndefinedDeep(v) : v).filter((v) => v !== void 0);
28
+ return Object.entries(object).map(([k, v]) => [k, v && typeof v === "object" ? stripUndefinedDeep(v) : v]).reduce((acc, [k, v]) => v === void 0 ? acc : {
29
+ ...acc,
30
+ [k]: v
31
+ }, {});
32
+ }
33
+ function deepMergeDefined(...objects) {
34
+ return deepmerge(...objects.map((v, i) => i === 0 ? v : stripUndefinedDeep(v)));
35
+ }
36
+ //#endregion
37
+ //#region src/lib/log.ts
38
+ /**
39
+ * The default logger instance for the library.
40
+ */
41
+ let log = createLogger({
42
+ logToConsole: { showTime: false },
43
+ name: "mdat"
44
+ });
45
+ setLogger$1(getChildLogger(log, "remark-mdat"));
46
+ setLogger$2(getChildLogger(log, "metascope"));
47
+ /**
48
+ * Set the logger instance for the module.
49
+ * Export this for library consumers to inject their own logger.
50
+ * @param logger - Accepts either a LogLayer instance or a Console- or Stream-like log target
51
+ */
52
+ function setLogger(logger) {
53
+ log = injectionHelper(logger);
54
+ setLogger$1(getChildLogger(log, "remark-mdat"));
55
+ setLogger$2(getChildLogger(log, "metascope"));
56
+ }
57
+ //#endregion
58
+ //#region src/lib/mdat-json-loader.ts
59
+ /**
60
+ * Lets arbitrary JSON objects (like from package.json) become reasonably good mdat rule sets
61
+ * HOWEVER cosmiconfig treats package.json as a special case and will always load only specific keys from it
62
+ * So we have to intercept and load them manually in config.ts
63
+ */
64
+ function mdatJsonLoader(filePath, content) {
65
+ const defaultJsonLoader = defaultLoaders[".json"];
66
+ return flattenJson(defaultJsonLoader(filePath, content));
67
+ }
68
+ function flattenJson(jsonObject, parentKey = "", result = {}) {
69
+ for (const [key, value] of Object.entries(jsonObject)) {
70
+ const fullPath = parentKey ? `${parentKey}.${key}` : key;
71
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) flattenJson(value, fullPath, result);
72
+ else if (value === null) result[fullPath] = "null";
73
+ else result[fullPath] = value.toString();
74
+ }
75
+ return result;
76
+ }
77
+ //#endregion
25
78
  //#region src/lib/context.ts
26
79
  let metascopeMetadata;
27
80
  /**
28
81
  * Get a bunch of platform-agnostic local metadata via metascope, exposed
29
- * primarily for plugin developers.
30
- * Result is memoized the result.
82
+ * primarily for plugin developers. Result is memoized the result.
83
+ *
31
84
  * @throws {Error} If no package.json is found
32
85
  */
33
86
  async function getContextMetadata() {
34
87
  if (metascopeMetadata !== void 0) return metascopeMetadata;
35
88
  metascopeMetadata = await getMetadata({
36
89
  absolute: false,
37
- offline: true
90
+ offline: true,
91
+ sources: [
92
+ "arduinoLibraryProperties",
93
+ "cinderCinderblockXml",
94
+ "codemetaJson",
95
+ "gitConfig",
96
+ "githubActions",
97
+ "goGoMod",
98
+ "goGoreleaserYaml",
99
+ "javaPomXml",
100
+ "licenseFile",
101
+ "metadataFile",
102
+ "metascope",
103
+ "nodePackageJson",
104
+ "obsidianPluginManifestJson",
105
+ "openframeworksAddonConfigMk",
106
+ "openframeworksInstallXml",
107
+ "processingLibraryProperties",
108
+ "processingSketchProperties",
109
+ "publiccodeYaml",
110
+ "pythonPkgInfo",
111
+ "pythonPyprojectToml",
112
+ "pythonSetupCfg",
113
+ "pythonSetupPy",
114
+ "readmeFile",
115
+ "rubyGemspec",
116
+ "rustCargoToml",
117
+ "xcodeInfoPlist",
118
+ "xcodeProjectPbxproj"
119
+ ]
38
120
  });
39
121
  return metascopeMetadata;
40
122
  }
123
+ const GIT_PREFIX_REGEX = /^git\+/;
124
+ const GIT_SUFFIX_REGEX = /\.git$/;
125
+ const TRAILING_SLASH_REGEX = /\/$/;
41
126
  /**
42
127
  * Reset
128
+ *
129
+ * @public
43
130
  */
44
131
  function resetContextMetadata() {
45
132
  metascopeMetadata = void 0;
@@ -50,7 +137,7 @@ const readmeMetadataTemplate = defineTemplate((context) => {
50
137
  const nodePackage = helpers.firstOf(nodePackageJson)?.data;
51
138
  const licenseFileData = helpers.firstOf(licenseFile);
52
139
  const ciActionFilePath = helpers.ensureArray(githubActions).find((entry) => entry.data.name.toLowerCase() === "ci")?.source;
53
- const repositoryOwner = codemeta.codeRepository ? new URL(codemeta.codeRepository).pathname.split("/")[1] : void 0;
140
+ const repositoryUrl = codemeta.codeRepository?.replace(GIT_PREFIX_REGEX, "").replace(GIT_SUFFIX_REGEX, "").replace(TRAILING_SLASH_REGEX, "");
54
141
  return {
55
142
  author: helpers.firstOf(helpers.mixedStringsToArray(helpers.toBasicNames(codemeta.author))),
56
143
  ciActionFileName: ciActionFilePath ? path.basename(ciActionFilePath) : void 0,
@@ -61,12 +148,14 @@ const readmeMetadataTemplate = defineTemplate((context) => {
61
148
  licenseFilePath: licenseFileData?.source,
62
149
  name: codemeta.name,
63
150
  projectDirectory: metascope?.data.options.path === void 0 ? void 0 : `file://${metascope.data.options.path}`,
64
- repositoryOwner
151
+ repositoryUrl
65
152
  };
66
153
  });
67
154
  let readmeMetadata;
68
155
  /**
69
156
  * Nice data for readme rules
157
+ *
158
+ * @public
70
159
  */
71
160
  async function getReadmeMetadata() {
72
161
  if (readmeMetadata !== void 0) return readmeMetadata;
@@ -75,62 +164,19 @@ async function getReadmeMetadata() {
75
164
  }
76
165
  /**
77
166
  * Reset
167
+ *
168
+ * @public
78
169
  */
79
170
  function resetReadmeMetadata() {
80
171
  readmeMetadata = void 0;
81
172
  }
82
- //#endregion
83
- //#region src/lib/deep-merge-defined.ts
84
- function stripUndefinedDeep(object) {
85
- if (Array.isArray(object)) return object.map((v) => v && typeof v === "object" ? stripUndefinedDeep(v) : v).filter((v) => v !== void 0);
86
- return Object.entries(object).map(([k, v]) => [k, v && typeof v === "object" ? stripUndefinedDeep(v) : v]).reduce((acc, [k, v]) => v === void 0 ? acc : {
87
- ...acc,
88
- [k]: v
89
- }, {});
90
- }
91
- function deepMergeDefined(...objects) {
92
- return deepmerge(...objects.map((v, i) => i === 0 ? v : stripUndefinedDeep(v)));
93
- }
94
- //#endregion
95
- //#region src/lib/log.ts
96
- /**
97
- * The default logger instance for the library.
98
- */
99
- let log = createLogger({
100
- logToConsole: { showTime: false },
101
- name: "mdat"
102
- });
103
- setLogger$1(getChildLogger(log, "remark-mdat"));
104
- setLogger$2(getChildLogger(log, "metascope"));
105
- /**
106
- * Set the logger instance for the module.
107
- * Export this for library consumers to inject their own logger.
108
- * @param logger - Accepts either a LogLayer instance or a Console- or Stream-like log target
109
- */
110
- function setLogger(logger) {
111
- log = injectionHelper(logger);
112
- setLogger$1(getChildLogger(log, "remark-mdat"));
113
- setLogger$2(getChildLogger(log, "metascope"));
114
- }
115
- //#endregion
116
- //#region src/lib/mdat-json-loader.ts
117
173
  /**
118
- * Lets arbitrary JSON objects (like from package.json) become reasonably good mdat rule sets
119
- * HOWEVER cosmiconfig treats package.json as a special case and will always load only specific keys from it
120
- * So we have to intercept and load them manually in config.ts
174
+ * Reset all cached metadata. Call between tests or when the underlying project
175
+ * files may have changed on disk.
121
176
  */
122
- function mdatJsonLoader(filePath, content) {
123
- const defaultJsonLoader = defaultLoaders[".json"];
124
- return flattenJson(defaultJsonLoader(filePath, content));
125
- }
126
- function flattenJson(jsonObject, parentKey = "", result = {}) {
127
- for (const [key, value] of Object.entries(jsonObject)) {
128
- const fullPath = parentKey ? `${parentKey}.${key}` : key;
129
- if (typeof value === "object" && value !== null && !Array.isArray(value)) flattenJson(value, fullPath, result);
130
- else if (value === null) result[fullPath] = "null";
131
- else result[fullPath] = value.toString();
132
- }
133
- return result;
177
+ function resetMetadataCaches() {
178
+ resetContextMetadata();
179
+ resetReadmeMetadata();
134
180
  }
135
181
  //#endregion
136
182
  //#region src/lib/readme/rules/badges.ts
@@ -143,13 +189,13 @@ var badges_default = { badges: { async content(options) {
143
189
  npm: z.array(z.string()).optional()
144
190
  }).optional().parse(options);
145
191
  const metadata = await getReadmeMetadata();
146
- const { ciActionFileName, license, name, repositoryOwner } = metadata;
192
+ const { ciActionFileName, license, name, repositoryUrl } = metadata;
147
193
  const badges = [];
148
194
  if (validOptions?.npm === void 0) {
149
195
  if (metadata.isPublicNpmPackage) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://npmjs.com/package/${name})`);
150
196
  } else for (const name of validOptions.npm) badges.push(`[![NPM Package ${name}](https://img.shields.io/npm/v/${name}.svg)](https://npmjs.com/package/${name})`);
151
197
  if (license !== void 0) badges.push(`[![License: ${license}](https://img.shields.io/badge/License-${license.replaceAll("-", "--")}-yellow.svg)](https://opensource.org/licenses/${license})`);
152
- if (ciActionFileName !== void 0 && repositoryOwner !== void 0) badges.push(`[![CI](https://github.com/${repositoryOwner}/${name}/actions/workflows/${ciActionFileName}/badge.svg)](https://github.com/${repositoryOwner}/${name}/actions/workflows/${ciActionFileName})`);
198
+ if (ciActionFileName !== void 0 && repositoryUrl !== void 0) badges.push(`[![CI](${repositoryUrl}/actions/workflows/${ciActionFileName}/badge.svg)](${repositoryUrl}/actions/workflows/${ciActionFileName})`);
153
199
  if (validOptions?.custom !== void 0) for (const [name, { image, link }] of Object.entries(validOptions.custom)) badges.push(`[![${name}](${image})](${link})`);
154
200
  return badges.join("\n");
155
201
  } } };
@@ -216,23 +262,20 @@ function isUrl(text, lenient = true) {
216
262
  if (typeof text !== "string") throw new TypeError("Expected a string");
217
263
  text = text.trim();
218
264
  if (text.includes(" ")) return false;
219
- try {
220
- new URL(text);
221
- return true;
222
- } catch {
223
- if (lenient) return isUrl(`https://${text}`, false);
224
- return false;
225
- }
265
+ if (URL.canParse(text)) return true;
266
+ if (lenient) return isUrl(`https://${text}`, false);
267
+ return false;
226
268
  }
227
269
  //#endregion
228
270
  //#region src/lib/readme/rules/code.ts
271
+ const LEADING_DOT_REGEX = /^\./;
229
272
  var code_default = { code: { async content(options) {
230
273
  const validOptions = z.object({
231
274
  file: z.string(),
232
275
  language: z.string().optional(),
233
276
  trim: z.boolean().default(true)
234
277
  }).parse(options);
235
- const lang = (path.extname(validOptions.file) ?? "").replace(/^\./, "");
278
+ const lang = (path.extname(validOptions.file) ?? "").replace(LEADING_DOT_REGEX, "");
236
279
  const exampleCode = await fs.readFile(path.join(process.cwd(), validOptions.file), "utf8");
237
280
  return `\`\`\`${lang}\n${validOptions.trim ? exampleCode.trim() : exampleCode}\n\`\`\``;
238
281
  } } };
@@ -287,8 +330,9 @@ var title_default = { title: {
287
330
  },
288
331
  order: 2
289
332
  } };
333
+ const SPLIT_FOR_TITLE_CASE_REGEX = /[ _-]+/;
290
334
  function makeTitleCase(text) {
291
- return text.split(/[ _-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
335
+ return text.split(SPLIT_FOR_TITLE_CASE_REGEX).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
292
336
  }
293
337
  //#endregion
294
338
  //#region src/lib/readme/rules/header.ts
@@ -467,6 +511,19 @@ var rules_default = {
467
511
  };
468
512
  //#endregion
469
513
  //#region src/lib/config.ts
514
+ let _configExplorer;
515
+ let _additionalConfigExplorer;
516
+ function getConfigExplorer() {
517
+ _configExplorer ??= cosmiconfig("mdat", { loaders: { ".ts": TypeScriptLoader() } });
518
+ return _configExplorer;
519
+ }
520
+ function getAdditionalConfigExplorer() {
521
+ _additionalConfigExplorer ??= cosmiconfig("mdat", { loaders: {
522
+ ".json": mdatJsonLoader,
523
+ ".ts": TypeScriptLoader()
524
+ } });
525
+ return _additionalConfigExplorer;
526
+ }
470
527
  /**
471
528
  * Load and validate mdat configuration.
472
529
  * Uses cosmiconfig to search in the usual places.
@@ -474,11 +531,9 @@ var rules_default = {
474
531
  */
475
532
  async function loadConfig(options) {
476
533
  const { additionalConfig, defaults = rules_default, searchFrom } = options ?? {};
477
- resetReadmeMetadata();
478
- resetContextMetadata();
479
534
  let finalConfig = { mdat: `Powered by the Markdown Autophagic Template system: [mdat](https://github.com/kitschpatrol/mdat).` };
480
535
  if (defaults) finalConfig = deepMergeDefined(finalConfig, defaults);
481
- const results = await cosmiconfig("mdat", { loaders: { ".ts": TypeScriptLoader() } }).search(searchFrom);
536
+ const results = await getConfigExplorer().search(searchFrom);
482
537
  if (results) {
483
538
  const { config, filepath } = results;
484
539
  let possibleRules = config;
@@ -493,10 +548,6 @@ async function loadConfig(options) {
493
548
  }
494
549
  if (additionalConfig !== void 0) {
495
550
  const additionalConfigArray = Array.isArray(additionalConfig) ? additionalConfig : [additionalConfig];
496
- const configExplorer2 = cosmiconfig("mdat", { loaders: {
497
- ".json": mdatJsonLoader,
498
- ".ts": TypeScriptLoader()
499
- } });
500
551
  for (const configOrPath of additionalConfigArray) {
501
552
  let loaded;
502
553
  if (typeof configOrPath === "string") {
@@ -505,7 +556,7 @@ async function loadConfig(options) {
505
556
  config: mdatJsonLoader(configOrPath, await fs.readFile(configOrPath, "utf8")),
506
557
  filepath: configOrPath
507
558
  };
508
- else results = await configExplorer2.load(configOrPath);
559
+ else results = await getAdditionalConfigExplorer().load(configOrPath);
509
560
  if (results === null || results === void 0) continue;
510
561
  const { config: loadedConfig, filepath } = results;
511
562
  log.debug(`Loaded additional config from "${filepath}"`);
@@ -541,20 +592,26 @@ function defineConfig(config) {
541
592
  }
542
593
  //#endregion
543
594
  //#region src/lib/format.ts
595
+ let cachedPrettier;
596
+ const configCache = /* @__PURE__ */ new Map();
544
597
  /**
545
598
  * Format a markdown string with Prettier, using config discovered from the file path.
546
599
  * Requires `prettier` to be installed as a peer dependency.
547
600
  */
548
601
  async function formatWithPrettier(content, filePath) {
549
- let prettier;
550
- try {
551
- prettier = await import("prettier");
602
+ if (cachedPrettier === void 0) try {
603
+ cachedPrettier = await import("prettier");
552
604
  } catch {
553
605
  throw new Error("The --format flag requires `prettier` to be installed. Run: pnpm add -D prettier");
554
606
  }
555
- const config = await prettier.resolveConfig(filePath ?? process.cwd());
556
- if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
557
- return prettier.format(content, {
607
+ const configKey = filePath ? path.dirname(filePath) : process.cwd();
608
+ let config = configCache.get(configKey);
609
+ if (config === void 0 && !configCache.has(configKey)) {
610
+ config = await cachedPrettier.resolveConfig(filePath ?? process.cwd());
611
+ configCache.set(configKey, config);
612
+ if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
613
+ }
614
+ return cachedPrettier.format(content, {
558
615
  ...config,
559
616
  filepath: filePath,
560
617
  parser: "markdown"
@@ -595,12 +652,13 @@ function ensureArray(value) {
595
652
  if (value === void 0 || value === null) return [];
596
653
  return Array.isArray(value) ? value : [value];
597
654
  }
655
+ const README_SEARCH_REGEX = /^readme(?:\.\w+)?$/i;
598
656
  /**
599
657
  * Finds a readme file in the current working directory (case-insensitive).
600
658
  */
601
659
  async function findReadme() {
602
660
  log.debug("Searching for readme in current directory...");
603
- const readme = (await fs.readdir(process.cwd())).find((entry) => /^readme(?:\.\w+)?$/i.test(entry));
661
+ const readme = (await fs.readdir(process.cwd())).find((entry) => README_SEARCH_REGEX.test(entry));
604
662
  if (readme !== void 0) {
605
663
  const absolutePath = path.resolve(readme);
606
664
  log.debug(`Found readme at "${absolutePath}"`);
@@ -616,7 +674,9 @@ async function findReadmeThrows() {
616
674
  if (readme === void 0) throw new Error("No readme found");
617
675
  return readme;
618
676
  }
677
+ let cachedAmbientRemarkConfig;
619
678
  async function loadAmbientRemarkConfig() {
679
+ if (cachedAmbientRemarkConfig !== void 0) return cachedAmbientRemarkConfig;
620
680
  const ambientConfig = new Configuration({
621
681
  cwd: process.cwd(),
622
682
  detectConfig: true,
@@ -637,34 +697,50 @@ async function loadAmbientRemarkConfig() {
637
697
  const { filePath } = configResult;
638
698
  if (filePath === void 0) log.debug("No ambient Remark configuration file found");
639
699
  else log.debug(`Found and loaded ambient Remark configuration from "${filePath}"`);
640
- return configResult;
700
+ cachedAmbientRemarkConfig = stripLintPlugins(configResult);
701
+ return cachedAmbientRemarkConfig;
641
702
  }
642
703
  log.debug("No ambient Remark configuration found");
643
- return {
704
+ cachedAmbientRemarkConfig = {
644
705
  filePath: void 0,
645
706
  plugins: [],
646
707
  settings: {}
647
708
  };
709
+ return cachedAmbientRemarkConfig;
710
+ }
711
+ /**
712
+ * Strip remark-lint plugins from an ambient config. Lint plugins only produce
713
+ * VFile warnings and never modify the AST or output — running them during
714
+ * expansion is pure overhead.
715
+ */
716
+ function stripLintPlugins(config) {
717
+ return {
718
+ ...config,
719
+ plugins: config.plugins.filter((entry) => {
720
+ const plugin = Array.isArray(entry) ? entry[0] : entry;
721
+ if (typeof plugin !== "function") return true;
722
+ const { name } = plugin;
723
+ return name !== "remarkLint" && !name.startsWith("remark-lint:") && name !== "remarkValidateLinks";
724
+ })
725
+ };
648
726
  }
649
727
  //#endregion
650
728
  //#region src/lib/processors.ts
651
729
  async function processFiles(files, loader, processorGetter, name, output, config) {
652
- const resolvedConfig = await loader({ additionalConfig: config });
653
- const localRemarkConfiguration = await loadAmbientRemarkConfig();
730
+ const [resolvedConfig, localRemarkConfiguration] = await Promise.all([loader({ additionalConfig: config }), loadAmbientRemarkConfig()]);
654
731
  const inputOutputPaths = await getInputOutputPaths(ensureArray(files), output, name, "md");
655
- const results = [];
656
732
  const resolvedProcessor = processorGetter(resolvedConfig, localRemarkConfiguration);
657
- for (const { input, name, output } of inputOutputPaths) {
733
+ return await Promise.all(inputOutputPaths.map(async ({ input, name, output }) => {
658
734
  const inputFile = await read(input);
659
735
  const result = await resolvedProcessor.process(inputFile);
660
736
  result.dirname = output;
661
737
  result.basename = name;
662
- results.push(result);
663
- }
664
- return results;
738
+ return result;
739
+ }));
665
740
  }
666
741
  async function processString(markdown, loader, processorGetter, config) {
667
- return processorGetter(await loader({ additionalConfig: config }), await loadAmbientRemarkConfig()).process(new VFile(markdown));
742
+ const [resolvedConfig, localRemarkConfiguration] = await Promise.all([loader({ additionalConfig: config }), loadAmbientRemarkConfig()]);
743
+ return processorGetter(resolvedConfig, localRemarkConfiguration).process(new VFile(markdown));
668
744
  }
669
745
  function getExpandProcessor(config, ambientRemarkConfig) {
670
746
  return remark().use({ settings: {
@@ -860,11 +936,10 @@ async function check(files, config, options) {
860
936
  files ??= await findReadmeThrows();
861
937
  const { read } = await import("to-vfile");
862
938
  const resolvedFiles = Array.isArray(files) ? files : [files];
863
- const originals = await Promise.all(resolvedFiles.map(async (f) => read(f)));
864
- const results = await processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config);
939
+ const [originals, results] = await Promise.all([Promise.all(resolvedFiles.map(async (f) => read(f))), processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config)]);
865
940
  if (options?.format) await formatResults(results);
866
941
  return results.map((result, i) => ({
867
- inSync: originals[i].toString() === result.toString(),
942
+ inSync: originals[i].toString().replaceAll("\r\n", "\n") === result.toString(),
868
943
  result
869
944
  }));
870
945
  }
@@ -872,4 +947,4 @@ async function formatResults(results) {
872
947
  for (const file of results) file.value = await formatWithPrettier(file.toString(), file.path || void 0);
873
948
  }
874
949
  //#endregion
875
- export { check, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, setLogger };
950
+ export { check, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdat",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
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",
@@ -48,9 +48,9 @@
48
48
  "cosmiconfig-typescript-loader": "^6.2.0",
49
49
  "deepmerge-ts": "^7.1.5",
50
50
  "globby": "^16.2.0",
51
- "lognow": "^0.5.2",
51
+ "lognow": "^0.6.0",
52
52
  "mdast-util-toc": "^7.1.0",
53
- "metascope": "^0.4.0",
53
+ "metascope": "^0.5.0",
54
54
  "path-type": "^6.0.0",
55
55
  "picocolors": "^1.1.1",
56
56
  "plur": "^6.0.0",
@@ -58,7 +58,7 @@
58
58
  "pretty-ms": "^9.3.0",
59
59
  "remark": "^15.0.1",
60
60
  "remark-gfm": "^4.0.1",
61
- "remark-mdat": "^2.0.0",
61
+ "remark-mdat": "^2.0.2",
62
62
  "to-vfile": "^8.0.0",
63
63
  "type-fest": "^5.5.0",
64
64
  "unified-engine": "^11.2.2",
@@ -69,13 +69,16 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "@arethetypeswrong/core": "^0.18.2",
72
- "@kitschpatrol/shared-config": "^6.2.0",
72
+ "@kitschpatrol/shared-config": "^7.0.0",
73
73
  "@types/mdast": "^4.0.4",
74
74
  "@types/node": "~22.17.2",
75
75
  "@types/unist": "^3.0.3",
76
76
  "@types/yargs": "^17.0.35",
77
77
  "bumpp": "^11.0.1",
78
78
  "execa": "^9.6.1",
79
+ "mdat-plugin-cli-help": "^2.0.2",
80
+ "mdat-plugin-example": "^2.0.0",
81
+ "mdat-plugin-tldraw": "^2.0.0",
79
82
  "nanoid": "^5.1.7",
80
83
  "prettier": "^3.8.1",
81
84
  "publint": "^0.3.18",
@@ -102,11 +105,13 @@
102
105
  }
103
106
  },
104
107
  "scripts": {
108
+ "bench": "vitest bench --run --compare test/benchmarks/baseline.json",
109
+ "bench:baseline": "vitest bench --run --outputJson test/benchmarks/baseline.json",
105
110
  "build": "tsdown",
106
111
  "clean": "git rm -f pnpm-lock.yaml ; git clean -fdX",
107
112
  "fix": "ksc fix",
108
113
  "lint": "ksc lint",
109
114
  "release": "bumpp --commit 'Release: %s' && pnpm run build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish",
110
- "test": "vitest run --no-file-parallelism"
115
+ "test": "vitest run"
111
116
  }
112
117
  }
package/readme.md CHANGED
@@ -1,5 +1,3 @@
1
- <!--+ Warning: Content inside HTML comment blocks was generated by mdat and may be overwritten. +-->
2
-
3
1
  <!-- title -->
4
2
 
5
3
  # mdat
@@ -10,6 +8,7 @@
10
8
 
11
9
  [![NPM Package mdat](https://img.shields.io/npm/v/mdat.svg)](https://npmjs.com/package/mdat)
12
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+ [![CI](https://github.com/kitschpatrol/mdat/actions/workflows/ci.yml/badge.svg)](https://github.com/kitschpatrol/mdat/actions/workflows/ci.yml)
13
12
 
14
13
  <!-- /badges -->
15
14
 
@@ -229,7 +228,7 @@ mdat [command] [files..] [options]
229
228
  | ---------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
230
229
  | `expand` | `[files..]` `[options]` | Expand MDAT placeholder comments. If no files are provided, the closest readme.md is expanded. _(Default command.)_ |
231
230
  | `collapse` | `[files..]` `[options]` | Collapse MDAT placeholder comments. If no files are provided, the closest readme.md is collapsed. |
232
- | `check` | `[files..]` `[options]` | Check if MDAT placeholder comments are up to date. Exits with code 1 if any files are out of sync. |
231
+ | `check` | `[files..]` `[options]` | Check if MDAT placeholder comments are up to date. Exits with code 1 if any files have stale or unexpanded content. |
233
232
  | `create` | `[options]` | Create a new Markdown file from a template. |
234
233
 
235
234
  _See the sections below for more information on each subcommand._
@@ -285,7 +284,7 @@ mdat collapse [files..] [options]
285
284
 
286
285
  #### Subcommand: `mdat check`
287
286
 
288
- Check if MDAT placeholder comments are up to date. Exits with code 1 if any files are out of sync.
287
+ Check if MDAT placeholder comments are up to date. Exits with code 1 if any files have stale or unexpanded content.
289
288
 
290
289
  Usage:
291
290
 
@@ -587,7 +586,7 @@ See the [Examples section](https://github.com/kitschpatrol/remark-mdat#examples)
587
586
  | File | Original | Gzip | Brotli |
588
587
  | ---------- | -------- | ------ | ------ |
589
588
  | .gitignore | 305 B | 245 B | 216 B |
590
- | readme.md | 18.1 kB | 6.4 kB | 5.4 kB |
589
+ | readme.md | 34.4 kB | 8.3 kB | 6.9 kB |
591
590
 
592
591
  <!-- /size-table -->
593
592
 
@@ -738,9 +737,9 @@ In 2.x, arguments **must** use function-call syntax with parentheses
738
737
 
739
738
  ### New functionality
740
739
 
741
- - The new --format flag runs expanded output through Prettier with local configuration before writing.
740
+ - The new `--format` flag runs expanded output through Prettier with local configuration before writing.
742
741
  - The badges rule now detects GitHub Actions CI workflows and includes a CI status badge automatically.
743
- - Check command re-implemented as a simplified dry-run expand + diff (exits 1 if out of sync)
742
+ - Check command reimplemented as a simplified dry-run expand and diff and reporting if content is unexpanded or out of date. Respects the `--format` flag.
744
743
 
745
744
  ### Rule shape changes
746
745
 
@@ -756,14 +755,52 @@ MDAT solves this by turning HTML comments in Markdown into placeholders for dyna
756
755
 
757
756
  ### Similar projects
758
757
 
759
- - Benjamin Lupton's [projectz](https://github.com/bevry/projectz)
760
- - David Wells' [Markdown Magic](https://github.com/DavidWells/markdown-magic)
761
- - Titus Wormer's [mdast-zone](https://github.com/syntax-tree/mdast-zone)
758
+ There's quite a bit of prior art and similar explorations of this problem space:
759
+
760
+ - Benjamin Lupton's [projectz](https://github.com/bevry/projectz)\
761
+ Goes way back.
762
+
763
+ - David Wells' [Markdown Magic](https://github.com/DavidWells/markdown-magic)\
764
+ I somehow missed the existence of this one until after building out MDAT. It's very similar conceptually, and has a nice ecosystem of plugins.
765
+
766
+ - Titus Wormer's [mdast-zone](https://github.com/syntax-tree/mdast-zone)\
767
+ Allows comments to be used as ranges or markers in Markdown files. Similar tree parsing and walking strategy to MDAT. Mdast-zone uses different syntax for arguments, and requires both opening and closing tags to be present for expansion to occur.
768
+
762
769
  - Jason Dent's [inject-markdown](https://github.com/streetsidesoftware/inject-markdown)
770
+
763
771
  - lillallol's [md-in-place](https://www.npmjs.com/package/md-in-place)
764
- - [AutoMD](https://automd.unjs.io/)
772
+
773
+ - [AutoMD](https://automd.unjs.io/)\
774
+ 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.
775
+
776
+ - Franck Abgrall's [readme-md-generator](https://github.com/kefranabg/readme-md-generator)
777
+
765
778
  - Anders Pitman's [tuplates](https://github.com/anderspitman/tuplates-py)
779
+
780
+ - VitePress' [Markdown file inclusion](https://vitepress.dev/guide/markdown#markdown-file-inclusion)
781
+
782
+ There's quite a bit of prior art and similar explorations of this problem space:
783
+
784
+ - Benjamin Lupton's [projectz](https://github.com/bevry/projectz)\
785
+ Goes way back.
786
+
787
+ - David Wells' [Markdown Magic](https://github.com/DavidWells/markdown-magic)\
788
+ I somehow missed the existence of this one until after building out MDAT. It's very similar conceptually, and has a nice ecosystem of plugins.
789
+
790
+ - Titus Wormer's [mdast-zone](https://github.com/syntax-tree/mdast-zone)\
791
+ Allows comments to be used as ranges or markers in Markdown files. Similar tree parsing and walking strategy to MDAT. Mdast-zone uses different syntax for arguments, and requires both opening and closing tags to be present for expansion to occur.
792
+
793
+ - Jason Dent's [inject-markdown](https://github.com/streetsidesoftware/inject-markdown)
794
+
795
+ - lillallol's [md-in-place](https://www.npmjs.com/package/md-in-place)
796
+
797
+ - [AutoMD](https://automd.unjs.io/)\
798
+ 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
+
766
800
  - Franck Abgrall's [readme-md-generator](https://github.com/kefranabg/readme-md-generator)
801
+
802
+ - Anders Pitman's [tuplates](https://github.com/anderspitman/tuplates-py)
803
+
767
804
  - VitePress' [Markdown file inclusion](https://vitepress.dev/guide/markdown#markdown-file-inclusion)
768
805
 
769
806
  ### Implementation notes