mdat 2.1.0 → 2.2.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 ADDED
Binary file
package/dist/bin/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import picocolors from "picocolors";
3
3
  import prettyMilliseconds from "pretty-ms";
4
- import { getMdatReports, getSoleRule, mdatCollapse, mdatExpand, mdatSplit, mdatStrip, reporterMdat, rulesSchema, setLogger } from "remark-mdat";
4
+ import { getMdatReports, getSoleRule, mdatCollapse, mdatDiff, mdatExpand, mdatSplit, mdatStrip, reporterMdat, rulesSchema, setLogger } from "remark-mdat";
5
+ import { remark } from "remark";
6
+ import remarkGfm from "remark-gfm";
5
7
  import { cosmiconfig, defaultLoaders } from "cosmiconfig";
6
8
  import { TypeScriptLoader } from "cosmiconfig-typescript-loader";
7
9
  import fs from "node:fs/promises";
@@ -17,8 +19,6 @@ import { promisify } from "node:util";
17
19
  import { brotliCompress, gzip } from "node:zlib";
18
20
  import prettyBytes from "pretty-bytes";
19
21
  import { toc } from "mdast-util-toc";
20
- import { remark } from "remark";
21
- import remarkGfm from "remark-gfm";
22
22
  import { read, write } from "to-vfile";
23
23
  import "vfile";
24
24
  import { Configuration } from "unified-engine";
@@ -42,7 +42,7 @@ function deepMergeDefined(...objects) {
42
42
  //#endregion
43
43
  //#region package.json
44
44
  var name = "mdat";
45
- var version = "2.1.0";
45
+ var version = "2.2.1";
46
46
 
47
47
  //#endregion
48
48
  //#region src/lib/log.ts
@@ -101,6 +101,7 @@ async function getContextMetadata() {
101
101
  metascopeMetadata = await getMetadata({
102
102
  absolute: false,
103
103
  offline: true,
104
+ recursive: false,
104
105
  sources: [
105
106
  "arduinoLibraryProperties",
106
107
  "cinderCinderblockXml",
@@ -603,7 +604,7 @@ async function formatWithPrettier(content, filePath) {
603
604
  if (config === void 0 && !configCache.has(configKey)) {
604
605
  config = await cachedPrettier.resolveConfig(filePath ?? process.cwd());
605
606
  configCache.set(configKey, config);
606
- if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
607
+ if (config) log.debug(`Resolved Prettier config for "${configKey}"`);
607
608
  }
608
609
  return cachedPrettier.format(content, {
609
610
  ...config,
@@ -662,7 +663,8 @@ async function findReadme() {
662
663
  }
663
664
  /**
664
665
  * Searches up for a readme.md file.
665
- * @throws {Error} if no readme is found
666
+ *
667
+ * @throws {Error} If no readme is found
666
668
  */
667
669
  async function findReadmeThrows() {
668
670
  const readme = await findReadme();
@@ -883,9 +885,8 @@ async function createReadme(options) {
883
885
  return readmePath;
884
886
  }
885
887
  function getTemplateForConfig(templateKey, compound) {
886
- const templateString = templates_default[templateKey].content[compound ? "compound" : "explicit"];
887
- if (templateString === void 0 || templateString === "") throw new Error(`No template found for "${templateKey}"`);
888
- return templateString;
888
+ if (!(templateKey in templates_default)) throw new Error(`Unknown template "${templateKey}". Available templates: ${Object.keys(templates_default).join(", ")}`);
889
+ return templates_default[templateKey].content[compound ? "compound" : "explicit"];
889
890
  }
890
891
  function getTemplateOptions() {
891
892
  return Object.entries(templates_default).map(([key, value]) => ({
@@ -949,10 +950,26 @@ async function check(files, config, options) {
949
950
  const resolvedFiles = Array.isArray(files) ? files : [files];
950
951
  const [originals, results] = await Promise.all([Promise.all(resolvedFiles.map(async (f) => read(f))), processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config)]);
951
952
  if (options?.format) await formatResults(results);
952
- return results.map((result, i) => ({
953
- inSync: originals[i].toString().replaceAll("\r\n", "\n") === result.toString(),
953
+ return results.map((result, i) => compareWithDiff(originals[i], result, options));
954
+ }
955
+ function compareWithDiff(original, result, options) {
956
+ const inSync = original.toString().replaceAll("\r\n", "\n") === result.toString();
957
+ if (!inSync) {
958
+ const parser = remark().use(remarkGfm);
959
+ const originalTree = parser.parse(original.toString());
960
+ mdatSplit(originalTree, original);
961
+ const expandedTree = parser.parse(result.toString());
962
+ mdatSplit(expandedTree, result);
963
+ const diffResults = mdatDiff(originalTree, original, expandedTree, result);
964
+ if (options?.format && diffResults.every((r) => r.status === "ok")) {
965
+ const message = result.message("Formatting differences outside mdat tags", { source: "diff" });
966
+ message.fatal = false;
967
+ }
968
+ }
969
+ return {
970
+ inSync,
954
971
  result
955
- }));
972
+ };
956
973
  }
957
974
  async function formatResults(results) {
958
975
  for (const file of results) file.value = await formatWithPrettier(file.toString(), file.path || void 0);
@@ -1087,6 +1104,7 @@ try {
1087
1104
  if (inSync) log.debug(`${picocolors.green("Up to date")}: ${filePath}`);
1088
1105
  else {
1089
1106
  log.warn(`${picocolors.red("Stale content")}: ${filePath}`);
1107
+ reporterMdat([result]);
1090
1108
  allInSync = false;
1091
1109
  }
1092
1110
  }
@@ -127,6 +127,16 @@ declare function check(files?: string | string[], config?: ConfigToLoad, options
127
127
  inSync: boolean;
128
128
  result: VFile;
129
129
  }>>;
130
+ /**
131
+ * Check if MDAT comments in a Markdown string are up to date by expanding and
132
+ * comparing per-tag.
133
+ */
134
+ declare function checkString(markdown: string, config?: ConfigToLoad, options?: {
135
+ format?: boolean;
136
+ }): Promise<{
137
+ inSync: boolean;
138
+ result: VFile;
139
+ }>;
130
140
  //#endregion
131
141
  //#region src/lib/context.d.ts
132
142
  /**
@@ -180,4 +190,4 @@ declare function resetMetadataCaches(): void;
180
190
  */
181
191
  declare function setLogger(logger?: ILogBasic | ILogLayer): void;
182
192
  //#endregion
183
- 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, strip, stripString };
193
+ export { type Config, type ReadmeMetadata, type Rule, check, checkString, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger, strip, stripString };
package/dist/lib/index.js CHANGED
@@ -1,10 +1,12 @@
1
+ import { remark } from "remark";
2
+ import remarkGfm from "remark-gfm";
3
+ import { getSoleRule, mdatCollapse, mdatDiff, mdatExpand, mdatSplit, mdatStrip, rulesSchema, setLogger as setLogger$1 } from "remark-mdat";
1
4
  import { cosmiconfig, defaultLoaders } from "cosmiconfig";
2
5
  import { TypeScriptLoader } from "cosmiconfig-typescript-loader";
3
6
  import fs from "node:fs/promises";
4
7
  import path from "node:path";
5
8
  import picocolors from "picocolors";
6
9
  import plur from "plur";
7
- import { getSoleRule, mdatCollapse, mdatExpand, mdatSplit, mdatStrip, rulesSchema, setLogger as setLogger$1 } from "remark-mdat";
8
10
  import { deepmerge } from "deepmerge-ts";
9
11
  import { createLogger, getChildLogger, injectionHelper } from "lognow";
10
12
  import { defineTemplate, getMetadata, helpers, setLogger as setLogger$2, templates } from "metascope";
@@ -15,8 +17,6 @@ import { promisify } from "node:util";
15
17
  import { brotliCompress, gzip } from "node:zlib";
16
18
  import prettyBytes from "pretty-bytes";
17
19
  import { toc } from "mdast-util-toc";
18
- import { remark } from "remark";
19
- import remarkGfm from "remark-gfm";
20
20
  import { read, write } from "to-vfile";
21
21
  import { VFile } from "vfile";
22
22
  import { Configuration } from "unified-engine";
@@ -88,6 +88,7 @@ async function getContextMetadata() {
88
88
  metascopeMetadata = await getMetadata({
89
89
  absolute: false,
90
90
  offline: true,
91
+ recursive: false,
91
92
  sources: [
92
93
  "arduinoLibraryProperties",
93
94
  "cinderCinderblockXml",
@@ -124,7 +125,8 @@ const GIT_PREFIX_REGEX = /^git\+/;
124
125
  const GIT_SUFFIX_REGEX = /\.git$/;
125
126
  const TRAILING_SLASH_REGEX = /\/$/;
126
127
  /**
127
- * Reset
128
+ * Reset cached context metadata. Call between tests or when the underlying
129
+ * project files may have changed on disk.
128
130
  *
129
131
  * @public
130
132
  */
@@ -163,7 +165,8 @@ async function getReadmeMetadata() {
163
165
  return readmeMetadata;
164
166
  }
165
167
  /**
166
- * Reset
168
+ * Reset cached readme metadata. Call between tests or when the underlying
169
+ * project files may have changed on disk.
167
170
  *
168
171
  * @public
169
172
  */
@@ -609,7 +612,7 @@ async function formatWithPrettier(content, filePath) {
609
612
  if (config === void 0 && !configCache.has(configKey)) {
610
613
  config = await cachedPrettier.resolveConfig(filePath ?? process.cwd());
611
614
  configCache.set(configKey, config);
612
- if (config) log.debug(`Using Prettier config from "${config.filepath}" for "${filePath ?? process.cwd()}"`);
615
+ if (config) log.debug(`Resolved Prettier config for "${configKey}"`);
613
616
  }
614
617
  return cachedPrettier.format(content, {
615
618
  ...config,
@@ -667,7 +670,8 @@ async function findReadme() {
667
670
  }
668
671
  /**
669
672
  * Searches up for a readme.md file.
670
- * @throws {Error} if no readme is found
673
+ *
674
+ * @throws {Error} If no readme is found
671
675
  */
672
676
  async function findReadmeThrows() {
673
677
  const readme = await findReadme();
@@ -883,9 +887,8 @@ async function createReadme(options) {
883
887
  return readmePath;
884
888
  }
885
889
  function getTemplateForConfig(templateKey, compound) {
886
- const templateString = templates_default[templateKey].content[compound ? "compound" : "explicit"];
887
- if (templateString === void 0 || templateString === "") throw new Error(`No template found for "${templateKey}"`);
888
- return templateString;
890
+ if (!(templateKey in templates_default)) throw new Error(`Unknown template "${templateKey}". Available templates: ${Object.keys(templates_default).join(", ")}`);
891
+ return templates_default[templateKey].content[compound ? "compound" : "explicit"];
889
892
  }
890
893
  function getTemplateOptions() {
891
894
  return Object.entries(templates_default).map(([key, value]) => ({
@@ -972,13 +975,40 @@ async function check(files, config, options) {
972
975
  const resolvedFiles = Array.isArray(files) ? files : [files];
973
976
  const [originals, results] = await Promise.all([Promise.all(resolvedFiles.map(async (f) => read(f))), processFiles(files, loadConfig, getExpandProcessor, void 0, void 0, config)]);
974
977
  if (options?.format) await formatResults(results);
975
- return results.map((result, i) => ({
976
- inSync: originals[i].toString().replaceAll("\r\n", "\n") === result.toString(),
978
+ return results.map((result, i) => compareWithDiff(originals[i], result, options));
979
+ }
980
+ /**
981
+ * Check if MDAT comments in a Markdown string are up to date by expanding and
982
+ * comparing per-tag.
983
+ */
984
+ async function checkString(markdown, config, options) {
985
+ const { VFile: VF } = await import("vfile");
986
+ const original = new VF(markdown);
987
+ const result = await processString(markdown, loadConfig, getExpandProcessor, config);
988
+ if (options?.format) await formatResults([result]);
989
+ return compareWithDiff(original, result, options);
990
+ }
991
+ function compareWithDiff(original, result, options) {
992
+ const inSync = original.toString().replaceAll("\r\n", "\n") === result.toString();
993
+ if (!inSync) {
994
+ const parser = remark().use(remarkGfm);
995
+ const originalTree = parser.parse(original.toString());
996
+ mdatSplit(originalTree, original);
997
+ const expandedTree = parser.parse(result.toString());
998
+ mdatSplit(expandedTree, result);
999
+ const diffResults = mdatDiff(originalTree, original, expandedTree, result);
1000
+ if (options?.format && diffResults.every((r) => r.status === "ok")) {
1001
+ const message = result.message("Formatting differences outside mdat tags", { source: "diff" });
1002
+ message.fatal = false;
1003
+ }
1004
+ }
1005
+ return {
1006
+ inSync,
977
1007
  result
978
- }));
1008
+ };
979
1009
  }
980
1010
  async function formatResults(results) {
981
1011
  for (const file of results) file.value = await formatWithPrettier(file.toString(), file.path || void 0);
982
1012
  }
983
1013
  //#endregion
984
- export { check, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger, strip, stripString };
1014
+ export { check, checkString, collapse, collapseString, createReadme as create, createReadmeInteractive as createInteractive, defineConfig, expand, expandString, getContextMetadata, getReadmeMetadata, loadConfig, mergeConfig, resetMetadataCaches, setLogger, strip, stripString };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdat",
3
- "version": "2.1.0",
3
+ "version": "2.2.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.6.0",
51
+ "lognow": "^0.6.1",
52
52
  "mdast-util-toc": "^7.1.0",
53
- "metascope": "^0.6.0",
53
+ "metascope": "^0.6.3",
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.1.0",
61
+ "remark-mdat": "^2.2.0",
62
62
  "to-vfile": "^8.0.0",
63
63
  "type-fest": "^5.5.0",
64
64
  "unified-engine": "^11.2.2",
@@ -69,23 +69,24 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "@arethetypeswrong/core": "^0.18.2",
72
- "@kitschpatrol/shared-config": "^7.0.1",
72
+ "@kitschpatrol/shared-config": "^7.1.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
+ "@vitest/coverage-v8": "4.1.2",
77
78
  "bumpp": "^11.0.1",
78
79
  "execa": "^9.6.1",
79
- "mdat-plugin-cli-help": "^2.0.2",
80
+ "mdat-plugin-cli-help": "^2.1.1",
80
81
  "mdat-plugin-example": "^2.0.0",
81
- "mdat-plugin-tldraw": "^2.0.0",
82
+ "mdat-plugin-tldraw": "^2.0.1",
82
83
  "nanoid": "^5.1.7",
83
84
  "prettier": "^3.8.1",
84
85
  "publint": "^0.3.18",
85
86
  "tsdown": "^0.21.7",
86
87
  "typescript": "~5.9.3",
87
88
  "unplugin-raw": "^0.7.0",
88
- "vitest": "^4.1.2"
89
+ "vitest": "^4.1.3"
89
90
  },
90
91
  "peerDependencies": {
91
92
  "prettier": "^3.0.0"
@@ -109,6 +110,7 @@
109
110
  "bench:baseline": "vitest bench --run --outputJson test/benchmarks/baseline.json",
110
111
  "build": "tsdown",
111
112
  "clean": "git rm -f pnpm-lock.yaml ; git clean -fdX",
113
+ "coverage": "vitest --coverage --run",
112
114
  "fix": "ksc fix",
113
115
  "lint": "ksc lint",
114
116
  "release": "bumpp --commit 'Release: %s' && pnpm run build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish",
package/readme.md CHANGED
@@ -413,19 +413,9 @@ mdat create
413
413
 
414
414
  `mdat` exports functions for expanding, collapsing, checking, and creating Markdown files programmatically.
415
415
 
416
- #### `expand`
416
+ #### `expand` / `expandString`
417
417
 
418
- ```ts
419
- function expand(
420
- files?: string | string[],
421
- name?: string,
422
- output?: string,
423
- config?: ConfigToLoad,
424
- options?: { format?: boolean },
425
- ): Promise<VFile[]>
426
- ```
427
-
428
- Expands MDAT comments in one or more files. If no files are provided, auto-finds the closest readme. Writing is the caller's responsibility:
418
+ Expands MDAT comments in one or more files. If no files are provided, auto-finds the closest readme. Writing is the caller's responsibility. Call `.toString()` on the returned [VFile](https://github.com/vfile) to get the result.
429
419
 
430
420
  ```ts
431
421
  import { expand } from 'mdat'
@@ -435,8 +425,6 @@ const [file] = await expand('readme.md')
435
425
  await write(file)
436
426
  ```
437
427
 
438
- #### `expandString`
439
-
440
428
  ```ts
441
429
  function expandString(
442
430
  markdown: string,
@@ -445,8 +433,6 @@ function expandString(
445
433
  ): Promise<VFile>
446
434
  ```
447
435
 
448
- Expands MDAT comments in a Markdown string. Call `.toString()` on the returned [VFile](https://github.com/vfile) to get the result.
449
-
450
436
  #### `collapse` / `collapseString`
451
437
 
452
438
  Removes expanded content, leaving only the opening comment placeholders. Same signatures as `expand` / `expandString`.
@@ -457,17 +443,9 @@ Strips all MDAT comment tags (both opening and closing) while preserving expande
457
443
 
458
444
  This is useful for producing a "clean" Markdown file that no longer depends on MDAT for future updates.
459
445
 
460
- #### `check`
461
-
462
- ```ts
463
- function check(
464
- files?: string | string[],
465
- config?: ConfigToLoad,
466
- options?: { format?: boolean },
467
- ): Promise<{ inSync: boolean; results: VFile[] }>
468
- ```
446
+ #### `check` / `checkString`
469
447
 
470
- Dry-run expand and compare with the file on disk. Returns `inSync: false` if the file would change.
448
+ Dry-run expand and compare with the file on disk. Returns `inSync: false` if the file would change. When stale, per-tag diagnostic messages are added to the result VFile identifying which specific tags are stale or unexpanded. Use `reporterMdat` from `remark-mdat` to display these messages.
471
449
 
472
450
  #### `create` / `createInteractive`
473
451
 
@@ -816,30 +794,6 @@ There's quite a bit of prior art and similar explorations of this problem space:
816
794
 
817
795
  - VitePress' [Markdown file inclusion](https://vitepress.dev/guide/markdown#markdown-file-inclusion)
818
796
 
819
- There's quite a bit of prior art and similar explorations of this problem space:
820
-
821
- - Benjamin Lupton's [projectz](https://github.com/bevry/projectz)\
822
- Goes way back.
823
-
824
- - David Wells' [Markdown Magic](https://github.com/DavidWells/markdown-magic)\
825
- 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.
826
-
827
- - Titus Wormer's [mdast-zone](https://github.com/syntax-tree/mdast-zone)\
828
- 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.
829
-
830
- - Jason Dent's [inject-markdown](https://github.com/streetsidesoftware/inject-markdown)
831
-
832
- - lillallol's [md-in-place](https://www.npmjs.com/package/md-in-place)
833
-
834
- - [AutoMD](https://automd.unjs.io/)\
835
- 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.
836
-
837
- - Franck Abgrall's [readme-md-generator](https://github.com/kefranabg/readme-md-generator)
838
-
839
- - Anders Pitman's [tuplates](https://github.com/anderspitman/tuplates-py)
840
-
841
- - VitePress' [Markdown file inclusion](https://vitepress.dev/guide/markdown#markdown-file-inclusion)
842
-
843
797
  ### Implementation notes
844
798
 
845
799
  This project was split from a monorepo containing both `mdat` and `remark-mdat` into separate repos in July 2024.