driftfence 0.0.2 → 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
 
@@ -70,13 +71,13 @@ Env vars:
70
71
 
71
72
  ## MVP Checks
72
73
 
73
- DriftFence checks `README.md` and `docs/**/*.md` for documentation drift.
74
+ DriftFence checks `README.md` and `docs/**/*.{md,mdx}` for documentation drift.
74
75
 
75
76
  Current checks:
76
77
 
77
78
  - package script references
78
79
  - file path references
79
- - env var references in Markdown docs
80
+ - env var references in Markdown and MDX docs
80
81
  - env var usage in source files
81
82
 
82
83
  Package script references include commands like:
@@ -124,9 +125,27 @@ DriftFence uses stable CLI exit codes:
124
125
  - `1` — documentation drift found
125
126
  - `2` — invalid project directory or CLI usage error
126
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
+
127
147
  ## Roadmap
128
148
 
129
- - MDX docs
130
149
  - GitHub Action
131
150
  - changed-files mode
132
151
  - configurable ignore rules
@@ -170,6 +170,30 @@ function getErrorMessage(error) {
170
170
  import { access } from "fs/promises";
171
171
  import { resolve } from "path";
172
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
+ ]);
173
197
  async function checkFilePaths(projectRoot, references) {
174
198
  const pathReferences = findFilePathReferences(references);
175
199
  const issues = [];
@@ -199,6 +223,9 @@ function findFilePathReferences(references) {
199
223
  function findFilePathReferencesInText(text, markdownPath) {
200
224
  const references = [];
201
225
  for (const match of text.matchAll(pathTokenPattern)) {
226
+ if (isPartialDottedToken(text, match)) {
227
+ continue;
228
+ }
202
229
  const path = cleanPathCandidate(match[1]);
203
230
  if (!isLikelyFilePath(path)) {
204
231
  continue;
@@ -213,17 +240,83 @@ function cleanPathCandidate(candidate) {
213
240
  return withoutUrlSuffix.replace(/:\d+(?::\d+)?$/g, "").replace(/^\.\//, "");
214
241
  }
215
242
  function isLikelyFilePath(path) {
216
- 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)) {
217
244
  return false;
218
245
  }
219
246
  if (!path.includes("/") && /^[A-Z][a-z]+(?:\.[a-z0-9]+)+$/.test(path)) {
220
247
  return false;
221
248
  }
249
+ if (isJavaScriptDottedIdentifier(path)) {
250
+ return false;
251
+ }
222
252
  return path.includes("/") || path.startsWith(".") || hasExtension(path);
223
253
  }
224
254
  function hasExtension(path) {
225
255
  return /(^|[/\\])[^/\\]+\.[A-Za-z][A-Za-z0-9]{1,7}$/.test(path);
226
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
+ }
227
320
  function uniquePathReferences(references) {
228
321
  const seen = /* @__PURE__ */ new Set();
229
322
  const uniqueReferences = [];
@@ -471,12 +564,15 @@ async function findMarkdownPathsInDirectory(directory) {
471
564
  paths.push(...await findMarkdownPathsInDirectory(path));
472
565
  continue;
473
566
  }
474
- if (entry.isFile() && entry.name.endsWith(".md")) {
567
+ if (entry.isFile() && isMarkdownDocument(entry.name)) {
475
568
  paths.push(path);
476
569
  }
477
570
  }
478
571
  return paths;
479
572
  }
573
+ function isMarkdownDocument(fileName) {
574
+ return fileName.endsWith(".md") || fileName.endsWith(".mdx");
575
+ }
480
576
  async function fileExists(path) {
481
577
  try {
482
578
  await readFile3(path, "utf8");
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  checkProject,
4
4
  formatReport
5
- } from "./chunk-WK3NGHEY.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-WK3NGHEY.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.2",
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
  },