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 +44 -5
- package/dist/{chunk-4RCBAET6.js → chunk-T6PL4YLE.js} +156 -30
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# DriftFence
|
|
2
2
|
|
|
3
3
|
[](https://github.com/CHAPAPOPA/driftfence/actions/workflows/ci.yml)
|
|
4
|
+
[](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
|
-
|
|
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(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
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
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftfence",
|
|
3
|
-
"version": "0.0.
|
|
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
|
+
}
|