Files
wehub-resource-sync f9447f8e5f
CI / typecheck · build · test · bundle-size (push) Failing after 1s
CI / clean-room dependency closure smoke (push) Failing after 1s
CI / server-runtime e2e (docker · pg + valkey) (push) Failing after 2s
Deploy Install Scripts / deploy (push) Failing after 2s
Windows / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:07:03 +08:00

485 lines
14 KiB
TypeScript

#!/usr/bin/env bun
import ts from 'typescript';
import postcss from 'postcss';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkMdx from 'remark-mdx';
import { visit } from 'unist-util-visit';
import { parse as parse5Parse, parseFragment as parse5ParseFragment } from 'parse5';
import { readFileSync, writeFileSync, statSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { extname, basename, join } from 'node:path';
import { parseArgs } from 'node:util';
interface CliOptions {
root: string;
check: boolean;
dryRun: boolean;
verbose: boolean;
}
function parseCliArgs(argv: string[]): CliOptions {
try {
const { values, positionals } = parseArgs({
args: argv.slice(2),
allowPositionals: true,
options: {
check: { type: 'boolean', default: false },
'dry-run': { type: 'boolean', default: false },
verbose: { type: 'boolean', short: 'v', default: false },
help: { type: 'boolean', short: 'h', default: false },
},
});
if (values.help) {
printHelp();
process.exit(0);
}
return {
root: positionals[0] ?? process.cwd(),
check: values.check,
dryRun: values['dry-run'],
verbose: values.verbose,
};
} catch (e) {
console.error((e as Error).message);
printHelp();
process.exit(2);
}
}
function printHelp(): void {
console.log(`Usage: bun scripts/strip-comments.ts [path] [flags]
Strips narrative comments from all git-tracked files using real parsers:
JS/TS/JSX -> TypeScript compiler (ts.getLeadingCommentRanges)
CSS/SCSS -> postcss + postcss-discard-comments
MD/MDX -> unified + remark-parse + remark-mdx + remark-stringify
HTML -> parse5 (parse, drop #comment nodes, serialize)
shell/py -> line-based hash stripper (kept; no library worth its weight)
Build directives are preserved (shebangs, @ts-*, eslint-disable, biome-ignore,
prettier-ignore, triple-slash references, webpack magic, /*! license keep).
Files containing @strip-comments-keep in the first 4096 bytes are skipped.
Flags:
--check Exit non-zero if any file would change. Doesn't write.
--dry-run Print what would change without writing.
--verbose Log each changed file.
-h, --help Show this help.
After running, review the diff with \`git diff\`, then run
\`npm run build-and-sync\` to confirm typecheck and build still pass.
`);
}
const SKIP_PATHS = new Set<string>([
'package-lock.json',
'bun.lock',
'bun.lockb',
'plugin/scripts/claude-mem',
]);
const SKIP_BASENAMES = new Set<string>([
'LICENSE',
'COPYING',
'NOTICE',
]);
const BINARY_EXT = new Set<string>([
'.svg', '.webp', '.woff', '.woff2', '.gif', '.png', '.jpg', '.jpeg', '.ico', '.pdf', '.zip',
]);
const JS_LIKE_EXT = new Set<string>(['.ts', '.tsx', '.js', '.jsx', '.cjs', '.mjs']);
const MD_EXT = new Set<string>(['.md', '.mdx']);
const HTML_EXT = new Set<string>(['.html', '.htm']);
const CSS_LIKE_EXT = new Set<string>(['.css', '.scss', '.less']);
const HASH_LIKE_EXT = new Set<string>(['.sh', '.bash', '.zsh', '.py']);
const HASH_LIKE_BASE = new Set<string>([
'.gitignore', '.npmignore', '.dockerignore', '.gitattributes', '.npmrc', '.editorconfig',
]);
const KEEP_MARKER = /@strip-comments-keep/;
const NUL_BYTE = 0;
function isDirectiveJs(text: string): boolean {
if (text.startsWith('///')) return true;
if (text.startsWith('/*!')) return true;
if (KEEP_MARKER.test(text)) return true;
const inner = text
.replace(/^\/\/\s*/, '')
.replace(/^\/\*+\s*/, '')
.replace(/\s*\*+\/$/, '')
.trim();
return /^(@ts-(?:ignore|expect-error|nocheck|check)\b|eslint-(?:disable|enable)|biome-ignore|prettier-ignore|@vitest-|c8\s+ignore|istanbul\s+ignore|@__PURE__|#__PURE__|webpack(?:ChunkName|Prefetch|Preload|Include|Exclude|Mode|Ignore))/.test(inner);
}
function scriptKindFor(ext: string): ts.ScriptKind {
switch (ext) {
case '.tsx': return ts.ScriptKind.TSX;
case '.jsx': return ts.ScriptKind.JSX;
case '.js':
case '.cjs':
case '.mjs': return ts.ScriptKind.JS;
default: return ts.ScriptKind.TS;
}
}
function parseDiagnosticsCount(sf: ts.SourceFile): number {
return ((sf as unknown as { parseDiagnostics?: ts.Diagnostic[] }).parseDiagnostics ?? []).length;
}
function stripJsLike(source: string, ext: string): string {
const kind = scriptKindFor(ext);
const sf = ts.createSourceFile('input', source, ts.ScriptTarget.Latest, true, kind);
const beforeErrs = parseDiagnosticsCount(sf);
const seen = new Set<string>();
const ranges: Array<[number, number]> = [];
function visitNode(node: ts.Node): void {
const leading = ts.getLeadingCommentRanges(source, node.getFullStart()) || [];
const trailing = ts.getTrailingCommentRanges(source, node.getEnd()) || [];
for (const r of leading) addRange(r);
for (const r of trailing) addRange(r);
ts.forEachChild(node, visitNode);
}
function addRange(r: ts.CommentRange): void {
const key = `${r.pos}-${r.end}`;
if (seen.has(key)) return;
seen.add(key);
const text = source.slice(r.pos, r.end);
if (isDirectiveJs(text)) return;
ranges.push([r.pos, r.end]);
}
visitNode(sf);
const out = spliceRanges(source, ranges);
const after = ts.createSourceFile('check', out, ts.ScriptTarget.Latest, true, kind);
const afterErrs = parseDiagnosticsCount(after);
if (afterErrs > beforeErrs) {
throw new Error(`strip introduced ${afterErrs - beforeErrs} new parse error(s); refusing to write`);
}
return out;
}
function collapseBlankLines(s: string): string {
return s.replace(/(?:[ \t]*\n){3,}/g, '\n\n');
}
function spliceRanges(source: string, ranges: Array<[number, number]>): string {
ranges.sort((a, b) => a[0] - b[0]);
let out = source;
for (let i = ranges.length - 1; i >= 0; i--) {
const [s, e] = ranges[i];
let removeStart = s;
let removeEnd = e;
let lineStart = s;
while (lineStart > 0 && (out[lineStart - 1] === ' ' || out[lineStart - 1] === '\t')) {
lineStart--;
}
if (lineStart === 0 || out[lineStart - 1] === '\n') {
removeStart = lineStart;
if (out[removeEnd] === '\n') removeEnd++;
}
out = out.slice(0, removeStart) + out.slice(removeEnd);
}
return collapseBlankLines(out);
}
function stripCss(source: string): string {
const root = postcss.parse(source);
const ranges: Array<[number, number]> = [];
root.walkComments((node) => {
const start = node.source?.start?.offset;
const end = node.source?.end?.offset;
if (typeof start !== 'number' || typeof end !== 'number') return;
const raw = source.slice(start, end);
if (raw.startsWith('/*!')) return;
if (KEEP_MARKER.test(raw)) return;
if (/\/\*\s*prettier-ignore/.test(raw)) return;
ranges.push([start, end]);
});
return spliceRanges(source, ranges);
}
const HTML_COMMENT_RE = /^<!--[\s\S]*-->$/;
function isMdxNarrativeComment(value: string): boolean {
const trimmed = value.trim();
if (/^\/\*[\s\S]*\*\/$/.test(trimmed)) return true;
if (/^\/\/.*$/.test(trimmed)) return true;
return false;
}
interface MdNode {
type: string;
value?: string;
position?: { start?: { offset?: number }; end?: { offset?: number } };
}
function stripMarkdown(source: string, isMdx: boolean): string {
const processor = unified().use(remarkParse);
if (isMdx) processor.use(remarkMdx);
const tree = processor.parse(source);
const ranges: Array<[number, number]> = [];
visit(tree, (node) => {
const n = node as MdNode;
const start = n.position?.start?.offset;
const end = n.position?.end?.offset;
if (typeof start !== 'number' || typeof end !== 'number') return;
if (n.type === 'html' && typeof n.value === 'string' && HTML_COMMENT_RE.test(n.value.trim())) {
if (KEEP_MARKER.test(n.value)) return;
ranges.push([start, end]);
return;
}
if (isMdx && (n.type === 'mdxFlowExpression' || n.type === 'mdxTextExpression')) {
const v = n.value ?? '';
if (isMdxNarrativeComment(v) && !KEEP_MARKER.test(v)) {
ranges.push([start, end]);
}
}
});
return spliceRanges(source, ranges);
}
interface Parse5Node {
nodeName: string;
childNodes?: Parse5Node[];
data?: string;
sourceCodeLocation?: { startOffset?: number; endOffset?: number };
}
function stripHtml(source: string, isFragment: boolean): string {
const tree = (isFragment
? parse5ParseFragment(source, { sourceCodeLocationInfo: true })
: parse5Parse(source, { sourceCodeLocationInfo: true })) as unknown as Parse5Node;
const ranges: Array<[number, number]> = [];
collectHtmlCommentRanges(tree, source, ranges);
return spliceRanges(source, ranges);
}
function collectHtmlCommentRanges(node: Parse5Node, source: string, ranges: Array<[number, number]>): void {
if (node.nodeName === '#comment') {
const start = node.sourceCodeLocation?.startOffset;
const end = node.sourceCodeLocation?.endOffset;
if (typeof start === 'number' && typeof end === 'number') {
const raw = source.slice(start, end);
if (!KEEP_MARKER.test(raw)) {
ranges.push([start, end]);
}
}
}
if (node.childNodes) {
for (const child of node.childNodes) {
collectHtmlCommentRanges(child, source, ranges);
}
}
}
function stripHashComments(source: string, preserveShebang: boolean): string {
const lines = source.split('\n');
const out: string[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (i === 0 && preserveShebang && line.startsWith('#!')) {
out.push(line);
continue;
}
const stripped = stripHashFromLine(line);
if (stripped === '' && line.trim().startsWith('#')) {
continue;
}
out.push(stripped);
}
return collapseBlankLines(out.join('\n'));
}
function stripHashFromLine(line: string): string {
let inSingle = false;
let inDouble = false;
let inBacktick = false;
for (let i = 0; i < line.length; i++) {
const c = line[i];
if (c === '\\' && i + 1 < line.length) {
i++;
continue;
}
if (!inDouble && !inBacktick && c === "'") inSingle = !inSingle;
else if (!inSingle && !inBacktick && c === '"') inDouble = !inDouble;
else if (!inSingle && !inDouble && c === '`') inBacktick = !inBacktick;
else if (!inSingle && !inDouble && !inBacktick && c === '#') {
if (i === 0 || /\s/.test(line[i - 1])) {
return line.slice(0, i).replace(/[ \t]+$/, '');
}
}
}
return line;
}
interface Stats {
changed: number;
unchanged: number;
skipped: number;
bytesBefore: number;
bytesAfter: number;
errors: string[];
changedFiles: string[];
reformatRatioFlags: string[];
}
function processFile(absPath: string, relPath: string, stats: Stats, opts: CliOptions): void {
if (SKIP_PATHS.has(relPath)) {
stats.skipped++;
return;
}
const base = basename(relPath);
if (SKIP_BASENAMES.has(base)) {
stats.skipped++;
return;
}
const ext = extname(relPath).toLowerCase();
if (BINARY_EXT.has(ext)) {
stats.skipped++;
return;
}
let st;
try {
st = statSync(absPath);
} catch {
stats.skipped++;
return;
}
if (!st.isFile()) {
stats.skipped++;
return;
}
let raw: Buffer;
try {
raw = readFileSync(absPath);
} catch {
stats.skipped++;
return;
}
if (raw.includes(NUL_BYTE)) {
stats.skipped++;
return;
}
const original = raw.toString('utf-8');
if (KEEP_MARKER.test(original.slice(0, 4096))) {
stats.skipped++;
return;
}
let stripped: string;
try {
if (JS_LIKE_EXT.has(ext)) {
stripped = stripJsLike(original, ext);
} else if (CSS_LIKE_EXT.has(ext)) {
stripped = stripCss(original);
} else if (MD_EXT.has(ext)) {
stripped = stripMarkdown(original, ext === '.mdx');
} else if (HTML_EXT.has(ext)) {
stripped = stripHtml(original, false);
} else if (
HASH_LIKE_EXT.has(ext) ||
HASH_LIKE_BASE.has(base) ||
base === 'Dockerfile' ||
base.startsWith('Dockerfile.')
) {
stripped = stripHashComments(original, true);
} else {
stats.skipped++;
return;
}
} catch (e: unknown) {
stats.errors.push(`${relPath}: ${(e as Error).message}`);
return;
}
if (stripped === original) {
stats.unchanged++;
return;
}
const removedBytes = original.length - stripped.length;
if (removedBytes > 0) {
const lineDiff = Math.abs(original.split('\n').length - stripped.split('\n').length);
const removedFraction = removedBytes / Math.max(original.length, 1);
if (lineDiff > 20 && removedFraction < 0.005) {
stats.reformatRatioFlags.push(relPath);
}
}
stats.changed++;
stats.bytesBefore += original.length;
stats.bytesAfter += stripped.length;
stats.changedFiles.push(relPath);
if (!opts.check && !opts.dryRun) {
writeFileSync(absPath, stripped, 'utf-8');
}
if (opts.verbose || opts.dryRun) {
console.log(`would change: ${relPath}`);
}
}
function main(): void {
const opts = parseCliArgs(process.argv);
const stats: Stats = {
changed: 0,
unchanged: 0,
skipped: 0,
bytesBefore: 0,
bytesAfter: 0,
errors: [],
changedFiles: [],
reformatRatioFlags: [],
};
let files: string[];
try {
files = execSync('git ls-files', { cwd: opts.root, encoding: 'utf-8' })
.split('\n')
.map((l) => l.trim())
.filter((l) => l.length > 0);
} catch (e) {
console.error(`git ls-files failed in ${opts.root}: ${(e as Error).message}`);
process.exit(2);
}
for (const rel of files) {
processFile(join(opts.root, rel), rel, stats, opts);
}
const suffix = opts.check ? ' (check mode, no writes)' : opts.dryRun ? ' (dry-run, no writes)' : '';
console.log(`Changed: ${stats.changed}${suffix}`);
console.log(`Unchanged: ${stats.unchanged}`);
console.log(`Skipped: ${stats.skipped}`);
if (stats.changed > 0) {
const saved = stats.bytesBefore - stats.bytesAfter;
const pct = ((saved / stats.bytesBefore) * 100).toFixed(1);
console.log(`Bytes: ${stats.bytesBefore} -> ${stats.bytesAfter} (-${saved}, -${pct}%)`);
}
if (stats.reformatRatioFlags.length > 0) {
console.log(`Reformat-suspect (${stats.reformatRatioFlags.length}): library may be reformatting more than stripping`);
for (const f of stats.reformatRatioFlags.slice(0, 10)) console.log(` ${f}`);
}
if (stats.errors.length > 0) {
console.log(`Errors (${stats.errors.length}):`);
for (const e of stats.errors.slice(0, 20)) {
console.log(` ${e}`);
}
}
if (stats.errors.length > 0) process.exit(1);
if (opts.check && stats.changed > 0) process.exit(1);
}
main();