mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
13b0c1ebf9
Co-authored-by: Andrew Barba <barba@hey.com>
89 lines
3.1 KiB
JavaScript
89 lines
3.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* CI lint that validates the `eve` import paths used in documentation code
|
|
* samples against the package's real `exports` map.
|
|
*
|
|
* Docs are the most-copied surface of the framework, and the cheapest way for
|
|
* a sample to rot is to import from a subpath that no longer exists (or never
|
|
* did). Full type-checking of every snippet is noisy because many blocks are
|
|
* intentional fragments; validating import specifiers against the exports map
|
|
* is deterministic and catches the highest-frequency failure with no false
|
|
* positives.
|
|
*
|
|
* Checks every ```ts / ```typescript fenced block under /docs: any
|
|
* `from "eve..."` / `import("eve...")` specifier must resolve to a real
|
|
* subpath in packages/eve/package.json#exports.
|
|
*/
|
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
const docsDir = `${repoRoot}/docs`;
|
|
const pkg = JSON.parse(readFileSync(`${repoRoot}/packages/eve/package.json`, "utf8"));
|
|
|
|
// Build the set of valid bare import specifiers from the exports map:
|
|
// "." -> "eve"
|
|
// "./tools" -> "eve/tools"
|
|
// "./channels/x" -> "eve/channels/x"
|
|
const validSpecifiers = new Set();
|
|
for (const key of Object.keys(pkg.exports ?? {})) {
|
|
if (key === "./package.json") continue;
|
|
validSpecifiers.add(key === "." ? "eve" : `eve/${key.slice(2)}`);
|
|
}
|
|
|
|
function walk(dir) {
|
|
const out = [];
|
|
for (const entry of readdirSync(dir)) {
|
|
const full = `${dir}/${entry}`;
|
|
if (statSync(full).isDirectory()) out.push(...walk(full));
|
|
else if (entry.endsWith(".md") || entry.endsWith(".mdx")) out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const fenceRe = /```(ts|tsx|typescript)\b[^\n]*\n([\s\S]*?)```/g;
|
|
// Capture the module specifier from static and dynamic imports/exports.
|
|
const specRe = /(?:from|import|export\s+\*\s+from)\s*\(?\s*["']([^"']+)["']/g;
|
|
|
|
const failures = [];
|
|
let blockCount = 0;
|
|
let specCount = 0;
|
|
|
|
for (const abs of walk(docsDir)) {
|
|
const rel = relative(docsDir, abs);
|
|
const source = readFileSync(abs, "utf8");
|
|
let block;
|
|
while ((block = fenceRe.exec(source)) !== null) {
|
|
blockCount += 1;
|
|
const code = block[2];
|
|
let m;
|
|
while ((m = specRe.exec(code)) !== null) {
|
|
const spec = m[1];
|
|
if (spec !== "eve" && !spec.startsWith("eve/")) continue; // only validate eve imports
|
|
specCount += 1;
|
|
if (!validSpecifiers.has(spec)) {
|
|
failures.push({ file: rel, spec });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (failures.length === 0) {
|
|
process.stdout.write(
|
|
`[docs:snippets] ok — ${specCount} eve import path${specCount === 1 ? "" : "s"} across ${blockCount} code blocks resolve.\n`,
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
process.stderr.write("[docs:snippets] FAIL\n\n");
|
|
for (const { file, spec } of failures) {
|
|
process.stderr.write(
|
|
` docs/${file}\n → imports \`${spec}\`, which is not an exported subpath of \`eve\`\n\n`,
|
|
);
|
|
}
|
|
process.stderr.write(
|
|
`Valid \`eve\` subpaths come from packages/eve/package.json#exports. Fix the import or add the export.\n`,
|
|
);
|
|
process.exit(1);
|