116 lines
3.3 KiB
JavaScript
116 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* check-forbidden-tokens.mjs
|
||
*
|
||
* Scans the codebase for forbidden tokens before publishing to npm.
|
||
* Mirrors the git pre-commit hook logic, but runs against the full
|
||
* working tree (not just staged changes).
|
||
*
|
||
* Token list: .git/hooks/forbidden-tokens.txt (one per line, # comments ok).
|
||
* If the file is missing, the check still uses the active local username when
|
||
* available. If username detection fails, the check degrades gracefully.
|
||
*/
|
||
|
||
import { execSync } from "node:child_process";
|
||
import { existsSync, readFileSync } from "node:fs";
|
||
import os from "node:os";
|
||
import { resolve } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
function uniqueNonEmpty(values) {
|
||
return Array.from(new Set(values.map((value) => value?.trim() ?? "").filter(Boolean)));
|
||
}
|
||
|
||
export function resolveDynamicForbiddenTokens(env = process.env, osModule = os) {
|
||
const candidates = [env.USER, env.LOGNAME, env.USERNAME];
|
||
|
||
try {
|
||
candidates.push(osModule.userInfo().username);
|
||
} catch {
|
||
// Some environments do not expose userInfo; env vars are enough fallback.
|
||
}
|
||
|
||
return uniqueNonEmpty(candidates);
|
||
}
|
||
|
||
export function readForbiddenTokensFile(tokensFile) {
|
||
if (!existsSync(tokensFile)) return [];
|
||
|
||
return readFileSync(tokensFile, "utf8")
|
||
.split("\n")
|
||
.map((line) => line.trim())
|
||
.filter((line) => line && !line.startsWith("#"));
|
||
}
|
||
|
||
export function resolveForbiddenTokens(tokensFile, env = process.env, osModule = os) {
|
||
return uniqueNonEmpty([
|
||
...resolveDynamicForbiddenTokens(env, osModule),
|
||
...readForbiddenTokensFile(tokensFile),
|
||
]);
|
||
}
|
||
|
||
export function runForbiddenTokenCheck({
|
||
repoRoot,
|
||
tokens,
|
||
exec = execSync,
|
||
log = console.log,
|
||
error = console.error,
|
||
}) {
|
||
if (tokens.length === 0) {
|
||
log(" ℹ Forbidden tokens list is empty — skipping check.");
|
||
return 0;
|
||
}
|
||
|
||
let found = false;
|
||
|
||
for (const token of tokens) {
|
||
try {
|
||
const result = exec(
|
||
`git grep -in --no-color -- ${JSON.stringify(token)} -- ':!pnpm-lock.yaml' ':!.git'`,
|
||
{ encoding: "utf8", cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"] },
|
||
);
|
||
if (result.trim()) {
|
||
if (!found) {
|
||
error("ERROR: Forbidden tokens found in tracked files:\n");
|
||
}
|
||
found = true;
|
||
const lines = result.trim().split("\n");
|
||
for (const line of lines) {
|
||
error(` ${line}`);
|
||
}
|
||
}
|
||
} catch {
|
||
// git grep returns exit code 1 when no matches — that's fine
|
||
}
|
||
}
|
||
|
||
if (found) {
|
||
error("\nBuild blocked. Remove the forbidden token(s) before publishing.");
|
||
return 1;
|
||
}
|
||
|
||
log(" ✓ No forbidden tokens found.");
|
||
return 0;
|
||
}
|
||
|
||
function resolveRepoPaths(exec = execSync) {
|
||
const repoRoot = exec("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
||
const gitDir = exec("git rev-parse --git-dir", { encoding: "utf8", cwd: repoRoot }).trim();
|
||
return {
|
||
repoRoot,
|
||
tokensFile: resolve(repoRoot, gitDir, "hooks/forbidden-tokens.txt"),
|
||
};
|
||
}
|
||
|
||
function main() {
|
||
const { repoRoot, tokensFile } = resolveRepoPaths();
|
||
const tokens = resolveForbiddenTokens(tokensFile);
|
||
process.exit(runForbiddenTokenCheck({ repoRoot, tokens }));
|
||
}
|
||
|
||
const isMainModule = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||
|
||
if (isMainModule) {
|
||
main();
|
||
}
|