driftfence 0.0.1 → 0.0.3

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
@@ -1,6 +1,7 @@
1
1
  # DriftFence
2
2
 
3
3
  [![CI](https://github.com/CHAPAPOPA/driftfence/actions/workflows/ci.yml/badge.svg)](https://github.com/CHAPAPOPA/driftfence/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/driftfence.svg)](https://www.npmjs.com/package/driftfence)
4
5
 
5
6
  Make sure your README doesn't lie.
6
7
 
@@ -8,7 +9,7 @@ DriftFence is a TypeScript Node.js CLI that catches outdated README and docs com
8
9
 
9
10
  ## Install
10
11
 
11
- Once published to npm:
12
+ Install from npm:
12
13
 
13
14
  ```sh
14
15
  npm install -D driftfence
@@ -24,10 +25,14 @@ npx driftfence check
24
25
 
25
26
  Check a specific project directory:
26
27
 
28
+ <!-- driftfence-ignore-start -->
29
+
27
30
  ```sh
28
31
  npx driftfence check ./path/to/project
29
32
  ```
30
33
 
34
+ <!-- driftfence-ignore-end -->
35
+
31
36
  ## Example Output
32
37
 
33
38
  The local clean demo exits with code 0:
@@ -44,6 +49,8 @@ npm run demo:drift
44
49
 
45
50
  Example output from the drift fixture:
46
51
 
52
+ <!-- driftfence-ignore-start -->
53
+
47
54
  ```text
48
55
  DriftFence found documentation drift.
49
56
 
@@ -60,19 +67,23 @@ Env vars:
60
67
  4 issues found.
61
68
  ```
62
69
 
70
+ <!-- driftfence-ignore-end -->
71
+
63
72
  ## MVP Checks
64
73
 
65
- DriftFence checks `README.md` and `docs/**/*.md` for documentation drift.
74
+ DriftFence checks `README.md` and `docs/**/*.{md,mdx}` for documentation drift.
66
75
 
67
76
  Current checks:
68
77
 
69
78
  - package script references
70
79
  - file path references
71
- - env var references in Markdown docs
80
+ - env var references in Markdown and MDX docs
72
81
  - env var usage in source files
73
82
 
74
83
  Package script references include commands like:
75
84
 
85
+ <!-- driftfence-ignore-start -->
86
+
76
87
  ```sh
77
88
  npm run build
78
89
  npm test
@@ -81,21 +92,31 @@ pnpm build
81
92
  yarn build
82
93
  ```
83
94
 
95
+ <!-- driftfence-ignore-end -->
96
+
84
97
  Env var checks currently support references like:
85
98
 
99
+ <!-- driftfence-ignore-start -->
100
+
86
101
  ```text
87
102
  API_URL
88
103
  DATABASE_URL
89
104
  VITE_API_URL
90
105
  ```
91
106
 
107
+ <!-- driftfence-ignore-end -->
108
+
92
109
  and source usages like:
93
110
 
111
+ <!-- driftfence-ignore-start -->
112
+
94
113
  ```ts
95
114
  process.env.API_URL
96
115
  import.meta.env.VITE_API_URL
97
116
  ```
98
117
 
118
+ <!-- driftfence-ignore-end -->
119
+
99
120
  ## Exit Codes
100
121
 
101
122
  DriftFence uses stable CLI exit codes:
@@ -104,9 +125,27 @@ DriftFence uses stable CLI exit codes:
104
125
  - `1` — documentation drift found
105
126
  - `2` — invalid project directory or CLI usage error
106
127
 
128
+ ## Ignoring intentional examples
129
+
130
+ Use ignore blocks when docs intentionally show fake paths, broken commands, fake env vars, or sample DriftFence output.
131
+
132
+ ````md
133
+ <!-- driftfence-ignore-start -->
134
+
135
+ ```sh
136
+ npm run missing-script
137
+ ```
138
+
139
+ See `docs/missing.md`.
140
+ Set `DATABASE_URL`.
141
+
142
+ <!-- driftfence-ignore-end -->
143
+ ````
144
+
145
+ Keep ignore blocks narrow so real setup instructions are still checked.
146
+
107
147
  ## Roadmap
108
148
 
109
- - MDX docs
110
149
  - GitHub Action
111
150
  - changed-files mode
112
151
  - configurable ignore rules
@@ -116,4 +155,4 @@ DriftFence uses stable CLI exit codes:
116
155
 
117
156
  AI features are not included in the MVP.
118
157
 
119
- DriftFence is deterministic-first: it checks concrete references in docs and code instead of guessing.
158
+ DriftFence is deterministic-first: it checks concrete references in docs and code instead of guessing.
@@ -126,7 +126,7 @@ async function readPackageJson(projectRoot) {
126
126
  };
127
127
  }
128
128
  try {
129
- return { packageJson: JSON.parse(rawPackageJson) };
129
+ return { packageJson: JSON.parse(stripBom(rawPackageJson)) };
130
130
  } catch (error) {
131
131
  return {
132
132
  issue: {
@@ -156,6 +156,9 @@ function normalizeCommand(command) {
156
156
  function hasOwn(object, key) {
157
157
  return Object.prototype.hasOwnProperty.call(object, key);
158
158
  }
159
+ function stripBom(content) {
160
+ return content.startsWith("\uFEFF") ? content.slice(1) : content;
161
+ }
159
162
  function getErrorCode(error) {
160
163
  return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
161
164
  }
@@ -167,6 +170,30 @@ function getErrorMessage(error) {
167
170
  import { access } from "fs/promises";
168
171
  import { resolve } from "path";
169
172
  var pathTokenPattern = /(?:^|[\s"'`(])((?:\.{1,2}[\\/][^\s"'`()<>[\]{}]+|[A-Za-z0-9_.-]+[\\/][^\s"'`()<>[\]{}]+|\.[A-Za-z0-9_-][A-Za-z0-9_.-]*|[A-Za-z0-9_-]+\.[A-Za-z][A-Za-z0-9]{1,7})(?:[?#][A-Za-z0-9_.:/#?=&-]+)?)/g;
173
+ var javascriptDottedIdentifierRoots = /* @__PURE__ */ new Set([
174
+ "Array",
175
+ "Boolean",
176
+ "Buffer",
177
+ "Date",
178
+ "JSON",
179
+ "Map",
180
+ "Math",
181
+ "Number",
182
+ "Object",
183
+ "Promise",
184
+ "Reflect",
185
+ "RegExp",
186
+ "Set",
187
+ "String",
188
+ "Symbol",
189
+ "WeakMap",
190
+ "WeakSet",
191
+ "console",
192
+ "globalThis",
193
+ "import",
194
+ "module",
195
+ "process"
196
+ ]);
170
197
  async function checkFilePaths(projectRoot, references) {
171
198
  const pathReferences = findFilePathReferences(references);
172
199
  const issues = [];
@@ -196,6 +223,9 @@ function findFilePathReferences(references) {
196
223
  function findFilePathReferencesInText(text, markdownPath) {
197
224
  const references = [];
198
225
  for (const match of text.matchAll(pathTokenPattern)) {
226
+ if (isPartialDottedToken(text, match)) {
227
+ continue;
228
+ }
199
229
  const path = cleanPathCandidate(match[1]);
200
230
  if (!isLikelyFilePath(path)) {
201
231
  continue;
@@ -210,17 +240,83 @@ function cleanPathCandidate(candidate) {
210
240
  return withoutUrlSuffix.replace(/:\d+(?::\d+)?$/g, "").replace(/^\.\//, "");
211
241
  }
212
242
  function isLikelyFilePath(path) {
213
- if (path.length === 0 || path.startsWith("@") || path.startsWith("-") || path.includes("://") || path.includes("*") || path.includes("$")) {
243
+ if (path.length === 0 || path.startsWith("@") || path.startsWith("-") || path.includes("://") || path.includes("*") || path.includes("$") || isUrlOrDomainLikeReference(path)) {
214
244
  return false;
215
245
  }
216
246
  if (!path.includes("/") && /^[A-Z][a-z]+(?:\.[a-z0-9]+)+$/.test(path)) {
217
247
  return false;
218
248
  }
249
+ if (isJavaScriptDottedIdentifier(path)) {
250
+ return false;
251
+ }
219
252
  return path.includes("/") || path.startsWith(".") || hasExtension(path);
220
253
  }
221
254
  function hasExtension(path) {
222
255
  return /(^|[/\\])[^/\\]+\.[A-Za-z][A-Za-z0-9]{1,7}$/.test(path);
223
256
  }
257
+ function isJavaScriptDottedIdentifier(path) {
258
+ if (path.startsWith(".") || path.includes("/") || path.includes("\\")) {
259
+ return false;
260
+ }
261
+ const parts = path.split(".");
262
+ return parts.length > 1 && javascriptDottedIdentifierRoots.has(parts[0] ?? "") && parts.every((part) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(part));
263
+ }
264
+ function isPartialDottedToken(text, match) {
265
+ const candidate = match[1] ?? "";
266
+ const matchText = match[0] ?? "";
267
+ const candidateStart = (match.index ?? 0) + Math.max(matchText.lastIndexOf(candidate), 0);
268
+ const candidateEnd = candidateStart + candidate.length;
269
+ return text[candidateEnd] === "." && /^[A-Za-z0-9_-]$/.test(text[candidateEnd + 1] ?? "");
270
+ }
271
+ function isUrlOrDomainLikeReference(path) {
272
+ return isUrlReference(path) || isEmailLikeReference(path) || isLocalhostReference(path) || isIpv4AddressReference(path) || isBareDomainReference(path);
273
+ }
274
+ function isUrlReference(path) {
275
+ return /^https?:\/\//i.test(path);
276
+ }
277
+ function isEmailLikeReference(path) {
278
+ return /^[^\s@/\\]+@[^\s@/\\]+\.[^\s@/\\]+$/.test(path);
279
+ }
280
+ function isLocalhostReference(path) {
281
+ return /^localhost(?::\d+)?(?:\/.*)?$/i.test(path);
282
+ }
283
+ function isIpv4AddressReference(path) {
284
+ return /^(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?(?:\/.*)?$/.test(path);
285
+ }
286
+ function isBareDomainReference(path) {
287
+ if (path.startsWith(".") || path.includes("\\") || path.includes("@")) {
288
+ return false;
289
+ }
290
+ const host = path.split("/")[0] ?? "";
291
+ return isDomainHost(host);
292
+ }
293
+ var commonDomainSuffixes = /* @__PURE__ */ new Set([
294
+ "ai",
295
+ "app",
296
+ "biz",
297
+ "co",
298
+ "com",
299
+ "dev",
300
+ "edu",
301
+ "gov",
302
+ "info",
303
+ "io",
304
+ "me",
305
+ "net",
306
+ "online",
307
+ "org",
308
+ "site",
309
+ "tv",
310
+ "xyz"
311
+ ]);
312
+ function isDomainHost(host) {
313
+ const withoutPort = host.replace(/:\d+$/, "");
314
+ const parts = withoutPort.split(".");
315
+ const suffix = parts.at(-1)?.toLowerCase();
316
+ return parts.length > 1 && suffix !== void 0 && commonDomainSuffixes.has(suffix) && parts.every(
317
+ (part) => /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(part)
318
+ );
319
+ }
224
320
  function uniquePathReferences(references) {
225
321
  const seen = /* @__PURE__ */ new Set();
226
322
  const uniqueReferences = [];
@@ -238,6 +334,52 @@ function uniquePathReferences(references) {
238
334
  // src/checkers/envVars.ts
239
335
  import { readFile as readFile2, readdir } from "fs/promises";
240
336
  import { join as join2, relative } from "path";
337
+
338
+ // src/markdown/extractMarkdownText.ts
339
+ import remarkParse from "remark-parse";
340
+ import { unified } from "unified";
341
+ var ignoreStart = "<!-- driftfence-ignore-start -->";
342
+ var ignoreEnd = "<!-- driftfence-ignore-end -->";
343
+ function extractMarkdownText(markdown) {
344
+ const tree = unified().use(remarkParse).parse(stripIgnoredMarkdownBlocks(markdown));
345
+ const references = [];
346
+ collectMarkdownText(tree, references);
347
+ return references;
348
+ }
349
+ function stripIgnoredMarkdownBlocks(markdown) {
350
+ let strippedMarkdown = "";
351
+ let position = 0;
352
+ while (position < markdown.length) {
353
+ const startIndex = markdown.indexOf(ignoreStart, position);
354
+ if (startIndex === -1) {
355
+ strippedMarkdown += markdown.slice(position);
356
+ break;
357
+ }
358
+ strippedMarkdown += markdown.slice(position, startIndex);
359
+ const endIndex = markdown.indexOf(
360
+ ignoreEnd,
361
+ startIndex + ignoreStart.length
362
+ );
363
+ if (endIndex === -1) {
364
+ break;
365
+ }
366
+ position = endIndex + ignoreEnd.length;
367
+ }
368
+ return strippedMarkdown;
369
+ }
370
+ function collectMarkdownText(node, references) {
371
+ if ((node.type === "code" || node.type === "inlineCode") && typeof node.value === "string") {
372
+ references.push({ kind: node.type, value: node.value });
373
+ }
374
+ if (!Array.isArray(node.children)) {
375
+ return;
376
+ }
377
+ for (const child of node.children) {
378
+ collectMarkdownText(child, references);
379
+ }
380
+ }
381
+
382
+ // src/checkers/envVars.ts
241
383
  var envVarPattern = /\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g;
242
384
  var processEnvPattern = /\bprocess\.env\.([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b/g;
243
385
  var importMetaEnvPattern = /\bimport\.meta\.env\.([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b/g;
@@ -273,11 +415,13 @@ function findEnvExampleNames(content) {
273
415
  }
274
416
  function findMarkdownEnvVarReferences(document) {
275
417
  return uniqueEnvVarReferences(
276
- [...document.content.matchAll(envVarPattern)].map((match) => ({
277
- name: match[0],
278
- source: "markdown",
279
- path: document.path
280
- }))
418
+ [...stripIgnoredMarkdownBlocks(document.content).matchAll(envVarPattern)].map(
419
+ (match) => ({
420
+ name: match[0],
421
+ source: "markdown",
422
+ path: document.path
423
+ })
424
+ )
281
425
  );
282
426
  }
283
427
  async function readEnvExampleNames(projectRoot) {
@@ -369,27 +513,6 @@ function getErrorCode2(error) {
369
513
  return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
370
514
  }
371
515
 
372
- // src/markdown/extractMarkdownText.ts
373
- import remarkParse from "remark-parse";
374
- import { unified } from "unified";
375
- function extractMarkdownText(markdown) {
376
- const tree = unified().use(remarkParse).parse(markdown);
377
- const references = [];
378
- collectMarkdownText(tree, references);
379
- return references;
380
- }
381
- function collectMarkdownText(node, references) {
382
- if ((node.type === "code" || node.type === "inlineCode") && typeof node.value === "string") {
383
- references.push({ kind: node.type, value: node.value });
384
- }
385
- if (!Array.isArray(node.children)) {
386
- return;
387
- }
388
- for (const child of node.children) {
389
- collectMarkdownText(child, references);
390
- }
391
- }
392
-
393
516
  // src/markdown/readMarkdownDocuments.ts
394
517
  import { readFile as readFile3, readdir as readdir2 } from "fs/promises";
395
518
  import { join as join3, relative as relative2 } from "path";
@@ -441,12 +564,15 @@ async function findMarkdownPathsInDirectory(directory) {
441
564
  paths.push(...await findMarkdownPathsInDirectory(path));
442
565
  continue;
443
566
  }
444
- if (entry.isFile() && entry.name.endsWith(".md")) {
567
+ if (entry.isFile() && isMarkdownDocument(entry.name)) {
445
568
  paths.push(path);
446
569
  }
447
570
  }
448
571
  return paths;
449
572
  }
573
+ function isMarkdownDocument(fileName) {
574
+ return fileName.endsWith(".md") || fileName.endsWith(".mdx");
575
+ }
450
576
  async function fileExists(path) {
451
577
  try {
452
578
  await readFile3(path, "utf8");
@@ -567,9 +693,9 @@ export {
567
693
  findPackageScriptReferences,
568
694
  checkFilePaths,
569
695
  findFilePathReferences,
696
+ extractMarkdownText,
570
697
  checkEnvVars,
571
698
  findEnvExampleNames,
572
- extractMarkdownText,
573
699
  readMarkdownDocuments,
574
700
  formatReport,
575
701
  checkProject
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  checkProject,
4
4
  formatReport
5
- } from "./chunk-4RCBAET6.js";
5
+ } from "./chunk-T6PL4YLE.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { stat } from "fs/promises";
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  findPackageScriptReferences,
10
10
  formatReport,
11
11
  readMarkdownDocuments
12
- } from "./chunk-4RCBAET6.js";
12
+ } from "./chunk-T6PL4YLE.js";
13
13
  export {
14
14
  checkEnvVars,
15
15
  checkFilePaths,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftfence",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "license": "MIT",
5
5
  "keywords": [
6
6
  "cli",
@@ -41,6 +41,7 @@
41
41
  "dev": "tsx src/cli.ts",
42
42
  "pack:dry": "npm pack --dry-run",
43
43
  "prepublishOnly": "npm run typecheck && npm test && npm run build",
44
+ "self:check": "node ./dist/cli.js check .",
44
45
  "test": "vitest run --pool=threads",
45
46
  "typecheck": "tsc --noEmit"
46
47
  },
@@ -65,4 +66,4 @@
65
66
  "typescript": "^6.0.3",
66
67
  "vitest": "^4.1.10"
67
68
  }
68
- }
69
+ }