markdown-links reports every local link as missing on Windows
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 71
- Forks
- 64
- Avg merge
- 15h 38m
- Merged PRs (30d)
- 66
Description
What happens
On a Windows checkout, node guards/run.mjs fails markdown-links with a violation for
every local link in the repository — 56 of them, including targets that plainly exist:
✗ markdown-links
CONTRIBUTING.md:12 local Markdown target "SECURITY.md" does not exist
README.md:53 local Markdown target "CONTRIBUTING.md" does not exist
README.md:521 local Markdown target "LICENSE" does not exist
... 53 more
3 guards ran, 1 failed.
actions-pinned passes in the same run, and the same checkout passes cleanly on Linux.
Because pnpm verify runs pnpm guards as a step, a Windows contributor cannot get a green
local verify, and the failure looks like a broken checkout rather than a platform bug.
Cause
guards/markdown-links.mjs takes the repository root from git:
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
git rev-parse --show-toplevel reports POSIX separators on every platform, so on Windows
this is C:/projects/facility. The containment check then compares it against a
path.resolve result, which uses native separators:
const exact = resolve(root, dirname(source), target); // C:\projects\facility\SECURITY.md
if (exact !== root && !exact.startsWith(`${root}${sep}`)) return false;
// ^^^^ "C:/projects/facility" + "\"
"C:\projects\facility\SECURITY.md".startsWith("C:/projects/facility\") is false, so the
function returns false before existsSync is ever called. Every in-repo target is reported
missing, and only on Windows.
Reproducible from any platform:
import { win32 } from "node:path";
const root = "C:/projects/facility"; // what git returns
const exact = win32.resolve(root, win32.dirname("README.md"), "SECURITY.md");
console.log(exact); // C:\projects\facility\SECURITY.md
console.log(exact.startsWith(`${root}${win32.sep}`)); // false <-- bails here
console.log(exact.startsWith(`${win32.resolve(root)}${win32.sep}`)); // true
Fix
Normalise the root with path.resolve before comparing. The containment rule itself is
unchanged, so targets that escape the repository are still rejected.
I have a PR ready: the resolution step is extracted as resolveWithinRoot with an injectable
path module so the Windows behaviour is testable from any host, plus five tests covering the
git-reported root, a native Windows root, POSIX parity, and traversal rejection on both
platforms.
Verified on the Windows machine where the failure was found — the same checkout goes from 56
violations to 2 guards ran, 0 failed — and node guards/run.mjs still passes on Linux.
Context
This is the third Windows papercut on the quickstart path, alongside #182 (pnpm dev fails
with spawn EINVAL) and #167 (Node 25+ no longer bundles corepack). Individually each is
small; together they mean a Windows contributor's first pnpm verify cannot go green. It might
be worth a Windows job in CI, even a minimal one that runs pnpm guards and pnpm test:dev —
all three of these would have been caught by it. Happy to open that separately if it's wanted.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with guards/markdown-links.mjs and inspect the containment check and the proposed resolveWithinRoot resolution step. Run node guards/run.mjs and the existing guard tests; done means local links pass on Windows and Linux while traversal targets remain rejected.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100