@datalackey/update-markdown-toc 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,8 @@
19
19
  - [Using npm scripts](#using-npm-scripts)
20
20
  - [Using a direct path (advanced)](#using-a-direct-path-advanced)
21
21
  - [Options](#options)
22
+ - [Configurable Exclusion List for Recursive Traversal](#configurable-exclusion-list-for-recursive-traversal)
23
+ - [Examples:](#examples)
22
24
  - [TOC Markers](#toc-markers)
23
25
  - [Usage Scenarios](#usage-scenarios)
24
26
  - [As Part of code/test/debug Work Flow](#as-part-of-codetestdebug-work-flow)
@@ -28,7 +30,6 @@
28
30
  - [Recursive Folder Traversal (Lenient Mode)](#recursive-folder-traversal-lenient-mode)
29
31
  - [Design Goals and Philosophy](#design-goals-and-philosophy)
30
32
  - [Guidelines For Project Contributors](#guidelines-for-project-contributors)
31
- - [Known limitations](#known-limitations)
32
33
  <!-- TOC:END -->
33
34
 
34
35
  ## Introduction
@@ -74,12 +75,13 @@ node_modules/.bin/ directory.
74
75
 
75
76
  After installation, the `update-markdown-toc` command can be invoked in any
76
77
  of the following ways from the project root (or a subdirectory) where the package was installed.
78
+ All examples below invoke the tool with the default README.md file as the TOC update target.
77
79
 
78
80
 
79
81
  ### Using npx (recommended)
80
82
 
81
83
  ```bash
82
- npx update-markdown-toc README.md
84
+ npx update-markdown-toc
83
85
  ````
84
86
 
85
87
 
@@ -90,7 +92,7 @@ You may also add a script entry to your package.json:
90
92
  ```json
91
93
  {
92
94
  "scripts": {
93
- "docs:toc": "update-markdown-toc README.md"
95
+ "docs:toc": "update-markdown-toc"
94
96
  }
95
97
  }
96
98
  ```
@@ -103,7 +105,7 @@ npm run docs:toc
103
105
  ### Using a direct path (advanced)
104
106
 
105
107
  ```bash
106
- ./node_modules/.bin/update-markdown-toc README.md
108
+ ./node_modules/.bin/update-markdown-toc
107
109
  ```
108
110
 
109
111
 
@@ -120,6 +122,7 @@ update-markdown-toc [options] [file]
120
122
  Options:
121
123
  -c, --check <path-to-file-or-folder> Do not write files; exit non-zero if TOC is stale
122
124
  -r, --recursive <path-to-folder> Recursively process all .md files under the given folder
125
+ -e, --exclude <dir1,dir2,...> Comma-separated list of directory names to exclude during recursive traversal (overrides default exclusion list)
123
126
  -v, --verbose Print status for every file processed
124
127
  -q, --quiet Suppress all non-error output
125
128
  -d, --debug Print debug diagnostics to stderr
@@ -129,6 +132,37 @@ Options:
129
132
  When using --check, a target file or a recursive folder must be specified
130
133
  explicitly. Unlike normal operation, --check does not default to README.md.
131
134
 
135
+ ### Configurable Exclusion List for Recursive Traversal
136
+
137
+ The `--exclude` option only applies when `--recursive` is specified.
138
+ It accepts a comma-separated list of directory names (exact match, not glob patterns), and
139
+ when provided, it replaces the default exclusion list.
140
+
141
+ By default, the recursive traversal excludes only: `node_modules`.
142
+
143
+ #### Examples:
144
+
145
+ Exclude node_modules and dist:
146
+
147
+ ```bash
148
+ npx update-markdown-toc --recursive . --exclude node_modules,dist
149
+ ```
150
+
151
+ Exclude only dist (this allows traversal into node_modules):
152
+
153
+ ```bash
154
+ npx update-markdown-toc --recursive . --exclude dist
155
+ ```
156
+
157
+ Disable exclusions entirely:
158
+
159
+ ```bash
160
+ npx update-markdown-toc --recursive . --exclude ""
161
+ ```
162
+
163
+
164
+
165
+
132
166
 
133
167
  ## TOC Markers
134
168
 
@@ -228,7 +262,7 @@ In this mode, files without TOC markers are silently skipped, and files with TOC
228
262
  (Rational: for larger repos, it may not be feasible to add TOC markers to every Markdown file at once.)
229
263
  Stale files are reported (unless --quiet is specified) and will
230
264
  result in a non-zero return value at the end of processing, but the
231
- script will not immediately bail wiht an error -- processing continues for all files found.
265
+ script will not immediately bail with an error -- processing continues for all files found.
232
266
 
233
267
  When combined with --verbose, skipped files (Markdown files without start/end region markers)
234
268
  are reported explicitly in this mode. For example:
@@ -251,12 +285,12 @@ This tool was designed in accordance with the top-level
251
285
  [Build Philosophy](../../README.md#build-philosophy) of this repository.
252
286
 
253
287
 
254
- - **update mode** (writes files: this should be run locally by developers, and should never run in CI)
288
+ - **update mode** (writes files: this should be run locally by developers, and should never run in CI. It is the default if `--check` not specified)
255
289
  - **check mode** (validates and exits non-zero if stale: mainly intended to be run in CI -- optional for local use)
256
290
 
257
291
  The intended workflow is:
258
292
 
259
- 1. Developers run the update command locally.
293
+ 1. Developers run the command locally in default mode (update mode, a.k.a. non `--check` mode) in their workspace
260
294
  2. Generated TOC content is reviewed and committed.
261
295
  3. CI runs in `--check` mode to ensure no drift exists.
262
296
 
@@ -266,8 +300,3 @@ The intended workflow is:
266
300
  Contributors to the project should consult [this document](GUIDELINES-FOR-PROJECT-CONTRIBUTORS.md)
267
301
 
268
302
 
269
- ## Known limitations
270
-
271
- - node_modules is excluded from recursive traversal: when using `--recursive` the tool will skip any directory
272
- named `node_modules` and its contents. This prevents accidental processing or modification of third-party files.
273
- This exclusion is currently hard-coded and not configurable via command-line flags or configuration files.
@@ -1,279 +1,57 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import fs from "fs";
4
- import path from "path";
5
- import { parseCli } from "./parseCli.js";
6
- import GithubSlugger from "github-slugger";
3
+ import { parseCli } from "../src/cli/parseCli.js";
4
+ import { runSingleFile, runRecursive } from "../src/index.js";
5
+ import { resolve } from "node:path";
6
+ import { existsSync, statSync } from "node:fs";
7
7
 
8
- /* ============================================================
9
- * Constants
10
- * ============================================================ */
8
+ function printHelp() {
9
+ console.log(`
10
+ update-markdown-toc [options] [file]
11
11
 
12
- const START = "<!-- TOC:START -->";
13
- const END = "<!-- TOC:END -->";
14
-
15
- /* ============================================================
16
- * CLI configuration
17
- * ============================================================ */
18
-
19
- const {
20
- checkMode,
21
- verbose,
22
- quiet,
23
- debug,
24
- recursivePath,
25
- targetFile,
26
- isRecursive
27
- } = parseCli();
28
-
29
- /* ============================================================
30
- * Debug helper
31
- * ============================================================ */
32
-
33
- function debugLog(msg) {
34
- if (debug) {
35
- console.error(`[debug] ${msg}`);
36
- }
37
- }
38
-
39
- /* ============================================================
40
- * Helpers
41
- * ============================================================ */
42
-
43
- function detectLineEnding(text) {
44
- return text.includes("\r\n") ? "\r\n" : "\n";
45
- }
46
-
47
- function collectMarkdownFiles(dir) {
48
- const results = [];
49
-
50
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
51
- const full = path.join(dir, entry.name);
52
-
53
- if (entry.isDirectory()) {
54
- // Exclude node_modules from recursive traversal to avoid processing third-party files
55
- if (entry.name === 'node_modules') continue;
56
-
57
- results.push(...collectMarkdownFiles(full));
58
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
59
- results.push(full);
60
- }
61
- }
62
-
63
- return results;
64
- }
65
-
66
- function generateTOC(content) {
67
- const lineEnding = detectLineEnding(content);
68
-
69
- const hasStart = content.includes(START);
70
- const hasEnd = content.includes(END);
71
-
72
- if (!hasStart && !hasEnd) {
73
- throw new Error("TOC delimiters not found");
74
- }
75
- if (hasStart && !hasEnd) {
76
- throw new Error("TOC start delimiter found without end");
77
- }
78
- if (!hasStart && hasEnd) {
79
- throw new Error("TOC end delimiter found without start");
80
- }
81
-
82
- const startIndex = content.indexOf(START);
83
- const endIndex = content.indexOf(END);
84
-
85
- const before = content.slice(0, startIndex);
86
- const after = content.slice(endIndex + END.length);
87
-
88
- // Preserve exactly one line boundary for parsing
89
- const contentWithoutTOC =
90
- before.replace(/\s*$/, "") +
91
- lineEnding +
92
- after.replace(/^\s*/, "");
93
-
94
- const lines = contentWithoutTOC.split(lineEnding);
95
- const headings = [];
96
-
97
- const slugger = new GithubSlugger();
98
-
99
- for (const line of lines) {
100
- const m = /^(#{1,6})\s+(.*)$/.exec(line);
101
- if (!m) continue;
102
-
103
- const level = m[1].length;
104
- const title = m[2].trim();
105
- const anchor = slugger.slug(title);
106
-
107
- headings.push({ level, title, anchor });
108
- }
109
-
110
- if (headings.length === 0) {
111
- throw new Error("No headings found to generate TOC");
112
- }
113
-
114
- const minLevel = Math.min(...headings.map(h => h.level));
115
- const tocLines = headings.map(h => {
116
- const indent = " ".repeat(h.level - minLevel);
117
- return `${indent}- [${h.title}](#${h.anchor})`;
118
- });
119
-
120
- const tocBlock =
121
- lineEnding +
122
- tocLines.join(lineEnding) +
123
- lineEnding;
124
-
125
- return (
126
- before +
127
- START +
128
- tocBlock +
129
- END +
130
- after
131
- );
12
+ Options:
13
+ -c, --check
14
+ -r, --recursive <path>
15
+ -e, --exclude <dir1,dir2,...>
16
+ -v, --verbose
17
+ -q, --quiet
18
+ -d, --debug
19
+ -h, --help
20
+ `);
132
21
  }
133
22
 
134
- /* ============================================================
135
- * File processing
136
- * ============================================================ */
137
-
138
- function processFile(filePath) {
139
- debugLog(`processing file: ${filePath}`);
140
-
141
- let content;
142
- try {
143
- content = fs.readFileSync(filePath, "utf8");
144
- } catch {
145
- const absolutePath = path.resolve(filePath);
146
- throw new Error(`Unable to read markdown file: ${absolutePath}`);
147
- }
148
-
149
- let updated;
150
- try {
151
- updated = generateTOC(content);
152
- } catch (err) {
153
- if (err.message === "TOC delimiters not found") {
154
- if (isRecursive) {
155
- debugLog("result: skipped (no markers)");
156
- return { status: "skipped" };
157
- }
158
- }
23
+ try {
24
+ const config = parseCli(process.argv.slice(2));
159
25
 
160
- const absolutePath = path.resolve(filePath);
161
- throw new Error(`${absolutePath}: ${err.message}`);
26
+ if (config.help) {
27
+ printHelp();
28
+ process.exit(0);
162
29
  }
163
30
 
164
- if (updated === content) {
165
- debugLog("result: unchanged");
166
- return { status: "unchanged" };
167
- }
31
+ if (config.isRecursive) {
32
+ const resolved = resolve(process.cwd(), config.recursivePath);
168
33
 
169
- if (checkMode) {
170
- debugLog("result: stale");
171
- return { status: "stale" };
172
- }
173
-
174
- fs.writeFileSync(filePath, updated, "utf8");
175
- debugLog("result: updated");
176
- return { status: "updated" };
177
- }
178
-
179
- /* ============================================================
180
- * Output
181
- * ============================================================ */
182
-
183
- function maybePrintStatus(status, filePath) {
184
- debugLog(`printing decision: status=${status}`);
185
-
186
- if (quiet) return;
187
-
188
- if (checkMode) {
189
- if (status === "stale") {
190
- console.log(`Stale: ${filePath}`);
191
- return;
34
+ if (!existsSync(resolved)) {
35
+ console.error("ERROR: Recursive path does not exist");
36
+ process.exit(1);
192
37
  }
193
38
 
194
- if (status === "skipped") {
195
- if (verbose) {
196
- console.log(`Skipped (no markers): ${filePath}`);
197
- }
198
- return;
39
+ if (!statSync(resolved).isDirectory()) {
40
+ console.error("ERROR: --recursive requires a directory");
41
+ process.exit(1);
199
42
  }
200
43
 
201
- if (status === "unchanged") {
202
- if (verbose) {
203
- console.log(`Up-to-date: ${filePath}`);
204
- }
205
- return;
206
- }
207
-
208
- return;
209
- }
210
-
211
- if (verbose) {
212
- if (status === "updated") {
213
- console.log(`Updated: ${filePath}`);
214
- } else if (status === "unchanged") {
215
- console.log(`Up-to-date: ${filePath}`);
216
- } else if (status === "skipped") {
217
- console.log(`Skipped (no markers): ${filePath}`);
218
- }
219
- return;
44
+ process.exit(runRecursive(resolved, config));
220
45
  }
221
46
 
222
- if (status === "updated") {
223
- console.log(`Updated: ${filePath}`);
224
- }
225
- }
226
-
227
- /* ============================================================
228
- * Execution
229
- * ============================================================ */
230
-
231
- let files = [];
232
-
233
- if (recursivePath) {
234
- const resolved = path.resolve(process.cwd(), recursivePath);
235
-
236
- if (!fs.existsSync(resolved)) {
237
- console.error("ERROR: Recursive path does not exist");
238
- process.exit(1);
239
- }
240
- if (!fs.statSync(resolved).isDirectory()) {
241
- console.error("ERROR: --recursive requires a directory");
242
- process.exit(1);
243
- }
244
-
245
- files = collectMarkdownFiles(resolved);
246
- files.sort();
247
- } else {
248
- const resolved = path.resolve(
47
+ const resolved = resolve(
249
48
  process.cwd(),
250
- targetFile || "README.md"
49
+ config.targetFile || "README.md"
251
50
  );
252
- files = [resolved];
253
- }
254
-
255
- let staleFound = false;
256
51
 
257
- for (const file of files) {
258
- try {
259
- const result = processFile(file);
52
+ process.exit(runSingleFile(resolved, config));
260
53
 
261
- if (checkMode && result.status === "stale") {
262
- staleFound = true;
263
- debugLog("staleFound set true");
264
- }
265
-
266
- maybePrintStatus(result.status, file);
267
- } catch (err) {
268
- console.error(`ERROR: ${err.message}`);
269
- process.exit(1);
270
- }
271
- }
272
-
273
- if (checkMode && staleFound) {
274
- debugLog("exiting with status 1 due to stale TOC");
54
+ } catch (err) {
55
+ console.error(`ERROR: ${err.message}`);
275
56
  process.exit(1);
276
- }
277
-
278
- debugLog("exiting with status 0");
279
- process.exit(0);
57
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@datalackey/update-markdown-toc",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Auto-generate Table of Contents for a Markdown file (or files, recursively from a top level folder)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "private": false,
8
8
  "scripts": {
9
9
  "test": "bash scripts/run-all-tests.sh",
10
- "prepack": "npm run test"
10
+ "prepack": "npm run test",
11
+ "test:unit": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js tests/unit"
11
12
  },
12
13
  "bin": {
13
14
  "update-markdown-toc": "./bin/update-markdown-toc.js"
@@ -45,5 +46,8 @@
45
46
  "dependencies": {
46
47
  "github-slugger": "^2.0.0",
47
48
  "ts-dedent": "^2.2.0"
49
+ },
50
+ "devDependencies": {
51
+ "jest": "^30.2.0"
48
52
  }
49
- }
53
+ }
package/bin/parseCli.js DELETED
@@ -1,97 +0,0 @@
1
- import { parseArgs } from "node:util";
2
- import { dedent } from "ts-dedent";
3
-
4
- export function parseCli() {
5
- let values, positionals;
6
-
7
- try {
8
- ({ values, positionals } = parseArgs({
9
- options: {
10
- check: { type: "boolean", short: "c" },
11
- recursive: { type: "string", short: "r" },
12
- verbose: { type: "boolean", short: "v" },
13
- quiet: { type: "boolean", short: "q" },
14
- debug: { type: "boolean", short: "d" },
15
- help: { type: "boolean", short: "h" }
16
- },
17
- allowShort: true,
18
- allowPositionals: true
19
- }));
20
- } catch (err) {
21
- console.error(`ERROR: ${err.message}`);
22
- process.exit(1);
23
- }
24
-
25
- if (values.help) {
26
- printHelp();
27
- process.exit(0);
28
- }
29
-
30
- const checkMode = values.check === true;
31
- const verbose = values.verbose === true;
32
- const quiet = values.quiet === true;
33
- const debug = values.debug === true;
34
-
35
- const recursivePath =
36
- typeof values.recursive === "string" ? values.recursive : null;
37
-
38
- let targetFile = null;
39
-
40
- if (positionals.length > 1) {
41
- console.error("ERROR: Only one file argument may be provided");
42
- process.exit(1);
43
- }
44
-
45
- if (positionals.length === 1) {
46
- targetFile = positionals[0];
47
- }
48
-
49
- // -------------------------
50
- // Contract validation
51
- // -------------------------
52
-
53
- if (quiet && verbose) {
54
- console.error("ERROR: --quiet and --verbose cannot be used together");
55
- process.exit(1);
56
- }
57
-
58
- if (checkMode && !recursivePath && !targetFile) {
59
- console.error("ERROR: --check requires a file or --recursive <path>");
60
- process.exit(1);
61
- }
62
-
63
- if (recursivePath && targetFile) {
64
- console.error("ERROR: Cannot use --recursive with a file argument");
65
- process.exit(1);
66
- }
67
-
68
- return Object.freeze({
69
- checkMode,
70
- verbose,
71
- quiet,
72
- debug,
73
-
74
- recursivePath,
75
- targetFile,
76
-
77
- isRecursive: Boolean(recursivePath)
78
- });
79
- }
80
-
81
- function printHelp() {
82
- console.log(dedent`
83
- update-markdown-toc [options] [file]
84
-
85
- Options:
86
- -c, --check <path-to-file-or-folder> Do not write files; exit non-zero if TOC is stale
87
- -r, --recursive <path-to-folder> Recursively process all .md files under the given folder
88
- -v, --verbose Print status for every file processed
89
- -q, --quiet Suppress all non-error output
90
- -d, --debug Print debug diagnostics to stderr
91
- -h, --help Show this help message and exit
92
-
93
- When using --check, a target file or a recursive folder must be specified
94
- explicitly. Unlike normal operation, --check does not default to README.md.
95
-
96
- `);
97
- }