opencode-anti-slop 0.1.1 → 0.1.4
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/.opencode/skills/verify-clean-code/SKILL.md +4 -1
- package/README.md +315 -275
- package/install.mjs +34 -2
- package/package.json +1 -1
- package/scripts/anti-slop/baseline.mjs +147 -0
- package/scripts/anti-slop/check-docs.mjs +109 -70
- package/scripts/anti-slop/check-duplicates.mjs +44 -19
- package/scripts/anti-slop/check-imports.mjs +72 -30
- package/scripts/anti-slop/check-modularity.mjs +72 -9
- package/scripts/anti-slop/cli.mjs +199 -115
- package/scripts/anti-slop/cli.test.mjs +98 -2
- package/scripts/anti-slop/generate-component-index.mjs +162 -133
- package/scripts/anti-slop/install-hooks.mjs +5 -7
- package/scripts/anti-slop/lib.mjs +47 -41
- package/scripts/anti-slop/run-project-checks.mjs +73 -0
- package/templates/anti-slop-ci.yml +1 -1
|
@@ -1,133 +1,162 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* generate-component-index.mjs
|
|
4
|
-
*
|
|
5
|
-
* Scans a source tree and (re)generates docs/COMPONENT_INDEX.md
|
|
6
|
-
* inventory that the `reuse-before-create` skill treats as the source of
|
|
7
|
-
* truth for existing layouts, components, hooks, and utilities.
|
|
8
|
-
*
|
|
9
|
-
* Zero dependencies (plain Node >= 18, ESM). Classification is heuristic
|
|
10
|
-
* (paths + export names + file extension); it is a discovery aid, not a parser.
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* node scripts/anti-slop/generate-component-index.mjs [--src src] [--out docs/COMPONENT_INDEX.md] [--root .]
|
|
14
|
-
*
|
|
15
|
-
* A "## Manual Notes" section at the end of an existing index is preserved.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
// --- args -------------------------------------------------------------------
|
|
29
|
-
function parseArgs(argv) {
|
|
30
|
-
const args = { src: 'src', out: 'docs/COMPONENT_INDEX.md', root: '.' };
|
|
31
|
-
for (let i = 2; i < argv.length; i++) {
|
|
32
|
-
const a = argv[i];
|
|
33
|
-
if (a === '--src') args.src = argv[++i];
|
|
34
|
-
else if (a === '--out') args.out = argv[++i];
|
|
35
|
-
else if (a === '--root') args.root = argv[++i];
|
|
36
|
-
else if (a === '
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
if (/(^|\/)
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
72
|
-
return '
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
md +=
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* generate-component-index.mjs
|
|
4
|
+
*
|
|
5
|
+
* Scans a source tree and (re)generates docs/COMPONENT_INDEX.md — the
|
|
6
|
+
* inventory that the `reuse-before-create` skill treats as the source of
|
|
7
|
+
* truth for existing layouts, components, hooks, and utilities.
|
|
8
|
+
*
|
|
9
|
+
* Zero dependencies (plain Node >= 18, ESM). Classification is heuristic
|
|
10
|
+
* (paths + export names + file extension); it is a discovery aid, not a parser.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/anti-slop/generate-component-index.mjs [--src src] [--out docs/COMPONENT_INDEX.md] [--root .]
|
|
14
|
+
*
|
|
15
|
+
* A "## Manual Notes" section at the end of an existing index is preserved.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
readFileSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
} from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { walk, extractExports } from './lib.mjs';
|
|
26
|
+
import { loadConfig, applyIgnorePaths } from './cli.mjs';
|
|
27
|
+
|
|
28
|
+
// --- args -------------------------------------------------------------------
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const args = { src: 'src', out: 'docs/COMPONENT_INDEX.md', root: '.', check: false };
|
|
31
|
+
for (let i = 2; i < argv.length; i++) {
|
|
32
|
+
const a = argv[i];
|
|
33
|
+
if (a === '--src') args.src = argv[++i];
|
|
34
|
+
else if (a === '--out') args.out = argv[++i];
|
|
35
|
+
else if (a === '--root') args.root = argv[++i];
|
|
36
|
+
else if (a === '--check') args.check = true;
|
|
37
|
+
else if (a === '-h' || a === '--help') {
|
|
38
|
+
console.log('Usage: node generate-component-index.mjs [--src src] [--out docs/COMPONENT_INDEX.md] [--root .] [--check]');
|
|
39
|
+
console.log(' --check compare against the file on disk; exit 1 when stale (CI mode, writes nothing)');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
} else {
|
|
42
|
+
console.error(`Unknown argument: ${a}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return args;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const args = parseArgs(process.argv);
|
|
50
|
+
|
|
51
|
+
const SRC_DIR = path.resolve(args.root, args.src);
|
|
52
|
+
const OUT_FILE = path.resolve(args.root, args.out);
|
|
53
|
+
|
|
54
|
+
if (!existsSync(SRC_DIR)) {
|
|
55
|
+
console.error(`Source directory not found: ${SRC_DIR}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- classification ---------------------------------------------------------
|
|
60
|
+
function classify(relPath, exports) {
|
|
61
|
+
const base = path.basename(relPath);
|
|
62
|
+
const dir = path.dirname(relPath).toLowerCase();
|
|
63
|
+
const baseName = path.basename(base, path.extname(base));
|
|
64
|
+
const isTsx = /\.(tsx|jsx)$/.test(base);
|
|
65
|
+
|
|
66
|
+
const hasJsxLike = exports.length > 0 && isTsx;
|
|
67
|
+
|
|
68
|
+
if (/^use[A-Z0-9]/.test(baseName) || /(^|\/)hooks?\//.test(dir)) return 'Hook';
|
|
69
|
+
if (/(^|\/)layouts?(\/|$)/.test(dir) || /layout/i.test(baseName)) return 'Layout';
|
|
70
|
+
if (hasJsxLike && exports.some((e) => /^[A-Z]/.test(e.name))) return 'Component';
|
|
71
|
+
if (/(^|\/)(services?|adapters?)(\/|$)/.test(dir)) return 'Service';
|
|
72
|
+
if (/(^|\/)(utils?|lib|helpers?)(\/|$)/.test(dir)) return 'Util';
|
|
73
|
+
if (exports.some((e) => e.kind === 'type')) return 'Types';
|
|
74
|
+
return 'Module';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- build ------------------------------------------------------------------
|
|
78
|
+
const files = walk(SRC_DIR, SRC_DIR);
|
|
79
|
+
// The generator and the docs gate must agree on what exists: docs.ignorePaths
|
|
80
|
+
// excludes framework-owned or declarative files from both the listing and the
|
|
81
|
+
// undocumented count, so the two never report different totals.
|
|
82
|
+
const ignorePaths = Array.isArray(loadConfig().docs?.ignorePaths)
|
|
83
|
+
? loadConfig().docs.ignorePaths
|
|
84
|
+
: [];
|
|
85
|
+
const filtered = applyIgnorePaths(files, ignorePaths, { label: 'docs.ignorePaths', src: args.src });
|
|
86
|
+
const index = new Map(); // kind -> [{rel, name, kind, desc, lines}]
|
|
87
|
+
let missingDocs = 0;
|
|
88
|
+
|
|
89
|
+
for (const file of filtered) {
|
|
90
|
+
const source = readFileSync(file.full, 'utf8');
|
|
91
|
+
const exports = extractExports(source);
|
|
92
|
+
if (exports.length === 0) continue;
|
|
93
|
+
const kind = classify(file.rel, exports);
|
|
94
|
+
const lines = source.split('\n').length;
|
|
95
|
+
const list = index.get(kind) ?? [];
|
|
96
|
+
for (const exp of exports) {
|
|
97
|
+
// The docs gate exempts default exports (route entry points etc.); the
|
|
98
|
+
// generator's count must agree, or the two report different totals.
|
|
99
|
+
if (!exp.desc && exp.kind !== 'default') missingDocs++;
|
|
100
|
+
list.push({ rel: file.rel, name: exp.name, kind: exp.kind, desc: exp.desc, lines });
|
|
101
|
+
}
|
|
102
|
+
index.set(kind, list);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- preserve manual notes --------------------------------------------------
|
|
106
|
+
let manualNotes = '';
|
|
107
|
+
if (existsSync(OUT_FILE)) {
|
|
108
|
+
const existing = readFileSync(OUT_FILE, 'utf8');
|
|
109
|
+
const marker = existing.indexOf('## Manual Notes');
|
|
110
|
+
if (marker !== -1) manualNotes = existing.slice(marker);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- render -----------------------------------------------------------------
|
|
114
|
+
const KIND_ORDER = ['Layout', 'Component', 'Hook', 'Service', 'Util', 'Types', 'Module'];
|
|
115
|
+
// Categories already plural keep their name; everything else takes a plain 's'.
|
|
116
|
+
const PLURAL = { Types: 'Types' };
|
|
117
|
+
const heading = (kind) => PLURAL[kind] ?? `${kind}s`;
|
|
118
|
+
const totalExports = KIND_ORDER.reduce((n, k) => n + (index.get(k)?.length ?? 0), 0);
|
|
119
|
+
|
|
120
|
+
let md = `# Component Index\n\n`;
|
|
121
|
+
md += `<!-- generated: run \`node scripts/anti-slop/generate-component-index.mjs\` to regenerate -->\n\n`;
|
|
122
|
+
md += `Inventory of shared layouts, components, hooks, services, and utilities under \`${args.src}/\`. `;
|
|
123
|
+
md += `${filtered.length} files, ${totalExports} exports.\n\n`;
|
|
124
|
+
md += `> Rule of use: before creating any new component, hook, or utility, check this file first (see the \`reuse-before-create\` skill). When you add a shared abstraction, rerun the generator and fill in the "When to use" column.\n\n`;
|
|
125
|
+
|
|
126
|
+
for (const kind of KIND_ORDER) {
|
|
127
|
+
const list = index.get(kind);
|
|
128
|
+
if (!list || list.length === 0) continue;
|
|
129
|
+
md += `## ${heading(kind)} (${list.length})\n\n`;
|
|
130
|
+
md += `| File | Export | When to use |\n|---|---|---|\n`;
|
|
131
|
+
for (const e of list) {
|
|
132
|
+
const doc = e.desc ? e.desc.replace(/\|/g, '\\|') : '';
|
|
133
|
+
md += `| \`${e.rel}\` | \`${e.name}\`${e.kind === 'type' ? ' *(type)*' : ''} | ${doc || '-'} |\n`;
|
|
134
|
+
}
|
|
135
|
+
md += `\n`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
md += manualNotes ? `${manualNotes.trimEnd()}\n` : `## Manual Notes\n\n(Add notes here; they are preserved when the index is regenerated.)\n`;
|
|
139
|
+
md = md.replace(/\n+$/, '\n');
|
|
140
|
+
|
|
141
|
+
if (args.check) {
|
|
142
|
+
if (!existsSync(OUT_FILE)) {
|
|
143
|
+
console.error(`[x] Index check FAILED: ${OUT_FILE} does not exist. Run the generator without --check and commit the result.`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const onDisk = readFileSync(OUT_FILE, 'utf8').replace(/\n+$/, '\n');
|
|
147
|
+
if (onDisk === md) {
|
|
148
|
+
console.log(`[ok] Component index is up to date (${OUT_FILE}).`);
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
console.error(`[x] Index check FAILED: ${OUT_FILE} is stale. Run \`node scripts/anti-slop/generate-component-index.mjs\` and commit the result.`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
mkdirSync(path.dirname(OUT_FILE), { recursive: true });
|
|
156
|
+
writeFileSync(OUT_FILE, md);
|
|
157
|
+
|
|
158
|
+
console.log(`Wrote ${OUT_FILE}`);
|
|
159
|
+
console.log(` ${filtered.length} source files, ${totalExports} exports across ${KIND_ORDER.filter((k) => index.has(k)).length} categories.`);
|
|
160
|
+
if (missingDocs > 0) {
|
|
161
|
+
console.log(` [doc] ${missingDocs} export(s) have no JSDoc description. Shared components/hooks/utils must carry a one-line "/** When to use ... */" above the export.`);
|
|
162
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Existing hooks are never overwritten without --force.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { existsSync,
|
|
15
|
+
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
|
|
18
18
|
const force = process.argv.includes('--force');
|
|
@@ -52,8 +52,8 @@ node scripts/anti-slop/check-docs.mjs $staged || exit 1
|
|
|
52
52
|
echo "[anti-slop] import-boundary report (non-blocking; cycles are tree-wide)..."
|
|
53
53
|
node scripts/anti-slop/check-imports.mjs --report-only || true
|
|
54
54
|
|
|
55
|
-
echo "[anti-slop] duplicate-code report (non-blocking):"
|
|
56
|
-
node scripts/anti-slop/check-duplicates.mjs --
|
|
55
|
+
echo "[anti-slop] duplicate-code report (non-blocking; sensitivity from anti-slop.config.json):"
|
|
56
|
+
node scripts/anti-slop/check-duplicates.mjs --threshold 0 --summary || true
|
|
57
57
|
|
|
58
58
|
exit 0
|
|
59
59
|
`;
|
|
@@ -66,10 +66,8 @@ node scripts/anti-slop/check-duplicates.mjs || exit 1
|
|
|
66
66
|
echo "[anti-slop] import-boundary gate..."
|
|
67
67
|
node scripts/anti-slop/check-imports.mjs || exit 1
|
|
68
68
|
|
|
69
|
-
echo "[anti-slop] project checks (
|
|
70
|
-
|
|
71
|
-
npm run typecheck --if-present || exit 1
|
|
72
|
-
npm test --if-present || exit 1
|
|
69
|
+
echo "[anti-slop] project checks (lint/typecheck/test; skips are printed, not hidden)..."
|
|
70
|
+
node scripts/anti-slop/run-project-checks.mjs || exit 1
|
|
73
71
|
|
|
74
72
|
exit 0
|
|
75
73
|
`;
|
|
@@ -44,11 +44,11 @@ export function walk(dir, base) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* Comment/string handling shared by both strippers below. Newlines (including
|
|
48
|
+
* those inside strings) are preserved, so line numbers computed on the output
|
|
49
|
+
* match the original source.
|
|
50
50
|
*/
|
|
51
|
-
|
|
51
|
+
function stripSource(src, blankStrings) {
|
|
52
52
|
let out = '';
|
|
53
53
|
let i = 0;
|
|
54
54
|
const n = src.length;
|
|
@@ -67,13 +67,28 @@ export function stripNoise(src) {
|
|
|
67
67
|
} else if (c === '"' || c === "'" || c === '`') {
|
|
68
68
|
const quote = c;
|
|
69
69
|
i++;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
if (blankStrings) {
|
|
71
|
+
while (i < n && src[i] !== quote) {
|
|
72
|
+
if (src[i] === '\\') i++;
|
|
73
|
+
if (src[i] === '\n') out += '\n';
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
i++; // closing quote
|
|
77
|
+
out += '""';
|
|
78
|
+
} else {
|
|
79
|
+
out += quote;
|
|
80
|
+
while (i < n && src[i] !== quote) {
|
|
81
|
+
if (src[i] === '\\' && i + 1 < n) {
|
|
82
|
+
out += src[i] + src[i + 1];
|
|
83
|
+
i += 2;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
out += src[i];
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
out += quote;
|
|
90
|
+
i++; // closing quote
|
|
74
91
|
}
|
|
75
|
-
i++; // closing quote
|
|
76
|
-
out += '""';
|
|
77
92
|
} else {
|
|
78
93
|
out += c;
|
|
79
94
|
i++;
|
|
@@ -83,32 +98,21 @@ export function stripNoise(src) {
|
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
/**
|
|
86
|
-
* Remove comments
|
|
87
|
-
*
|
|
88
|
-
*
|
|
101
|
+
* Remove comments and blank string contents so brace/regex analysis is
|
|
102
|
+
* reliable. Newlines are preserved, so line numbers computed on the output
|
|
103
|
+
* match the original source.
|
|
104
|
+
*/
|
|
105
|
+
export function stripNoise(src) {
|
|
106
|
+
return stripSource(src, true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Remove comments while KEEPING string contents verbatim — used where the
|
|
111
|
+
* text inside strings matters (import specifier parsing) but comment noise
|
|
112
|
+
* must not create false edges.
|
|
89
113
|
*/
|
|
90
114
|
export function stripComments(src) {
|
|
91
|
-
|
|
92
|
-
let i = 0;
|
|
93
|
-
const n = src.length;
|
|
94
|
-
while (i < n) {
|
|
95
|
-
const c = src[i];
|
|
96
|
-
const next = src[i + 1];
|
|
97
|
-
if (c === '/' && next === '/') {
|
|
98
|
-
while (i < n && src[i] !== '\n') i++;
|
|
99
|
-
} else if (c === '/' && next === '*') {
|
|
100
|
-
i += 2;
|
|
101
|
-
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
102
|
-
if (src[i] === '\n') out += '\n';
|
|
103
|
-
i++;
|
|
104
|
-
}
|
|
105
|
-
i += 2;
|
|
106
|
-
} else {
|
|
107
|
-
out += c;
|
|
108
|
-
i++;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return out;
|
|
115
|
+
return stripSource(src, false);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
const TOKEN_RE =
|
|
@@ -181,24 +185,26 @@ export function jsdocBefore(source, idx) {
|
|
|
181
185
|
*/
|
|
182
186
|
export function extractExports(source) {
|
|
183
187
|
const found = [];
|
|
184
|
-
const add = (name, kind, index) => {
|
|
185
|
-
if (name && !found.some((e) => e.name === name))
|
|
188
|
+
const add = (name, kind, index, isDefault) => {
|
|
189
|
+
if (name && !found.some((e) => e.name === name)) {
|
|
190
|
+
found.push({ name, kind, index, isDefault: Boolean(isDefault) });
|
|
191
|
+
}
|
|
186
192
|
};
|
|
187
193
|
|
|
188
194
|
for (const m of source.matchAll(/export\s+(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z0-9_$]+)/g)) {
|
|
189
|
-
add(m[1], 'function', m.index);
|
|
195
|
+
add(m[1], 'function', m.index, m[0].startsWith('export default'));
|
|
190
196
|
}
|
|
191
197
|
for (const m of source.matchAll(/export\s+(?:default\s+)?const\s+([A-Za-z0-9_$]+)/g)) {
|
|
192
|
-
add(m[1], /=>/.test(source.slice(m.index, m.index + 200)) ? 'function' : 'value', m.index);
|
|
198
|
+
add(m[1], /=>/.test(source.slice(m.index, m.index + 200)) ? 'function' : 'value', m.index, m[0].startsWith('export default'));
|
|
193
199
|
}
|
|
194
200
|
for (const m of source.matchAll(/export\s+(?:abstract\s+)?class\s+([A-Za-z0-9_$]+)/g)) {
|
|
195
|
-
add(m[1], 'class', m.index);
|
|
201
|
+
add(m[1], 'class', m.index, m[0].startsWith('export default'));
|
|
196
202
|
}
|
|
197
203
|
for (const m of source.matchAll(/export\s+(?:type|interface)\s+([A-Za-z0-9_$]+)/g)) {
|
|
198
|
-
add(m[1], 'type', m.index);
|
|
204
|
+
add(m[1], 'type', m.index, false);
|
|
199
205
|
}
|
|
200
206
|
if (/export\s+default\s+(?!function|class|const)/.test(source)) {
|
|
201
|
-
add('default', 'default', source.length);
|
|
207
|
+
add('default', 'default', source.length, true);
|
|
202
208
|
}
|
|
203
209
|
return found.map((e) => ({ ...e, desc: jsdocBefore(source, e.index) }));
|
|
204
210
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* run-project-checks.mjs — pre-push project checks, without silent skips.
|
|
4
|
+
*
|
|
5
|
+
* Runs the repo's own lint/typecheck/test scripts with the package manager
|
|
6
|
+
* detected from the lockfile, and:
|
|
7
|
+
* - prints every check as present or skipped, so "nothing ran" is visible;
|
|
8
|
+
* - falls back to `tsc --noEmit` when there is no typecheck script but a
|
|
9
|
+
* tsconfig.json exists (a TypeScript repo must not pass untypechecked);
|
|
10
|
+
* - works with npm, pnpm, yarn and bun.
|
|
11
|
+
*
|
|
12
|
+
* Zero dependencies. Exits non-zero on the first failing check.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
const pkg = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
21
|
+
const scripts = pkg.scripts ?? {};
|
|
22
|
+
|
|
23
|
+
function detectPackageManager() {
|
|
24
|
+
if (existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
25
|
+
if (existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
|
|
26
|
+
if (existsSync(path.join(cwd, 'bun.lockb')) || existsSync(path.join(cwd, 'bun.lock'))) return 'bun';
|
|
27
|
+
return 'npm';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pm = detectPackageManager();
|
|
31
|
+
const run = (cmd, args) => spawnSync(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32' });
|
|
32
|
+
|
|
33
|
+
/** Run `<pm> run <script>` when it exists. Returns 'ran' | 'skipped'. */
|
|
34
|
+
function runScript(name) {
|
|
35
|
+
if (!scripts[name]) {
|
|
36
|
+
console.log(`[skip] ${name}: no "${name}" script in package.json`);
|
|
37
|
+
return 'skipped';
|
|
38
|
+
}
|
|
39
|
+
console.log(`[run] ${name} (${pm} run ${name})`);
|
|
40
|
+
const res = run(pm, ['run', name]);
|
|
41
|
+
if (res.status !== 0) {
|
|
42
|
+
console.error(`\n[x] "${name}" failed (exit ${res.status ?? 'signal'}).`);
|
|
43
|
+
process.exit(res.status ?? 1);
|
|
44
|
+
}
|
|
45
|
+
return 'ran';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** tsc --noEmit fallback for TypeScript repos without a typecheck script. */
|
|
49
|
+
function runTypecheck() {
|
|
50
|
+
if (scripts.typecheck) return runScript('typecheck');
|
|
51
|
+
|
|
52
|
+
const hasTs = existsSync(path.join(cwd, 'tsconfig.json')) ||
|
|
53
|
+
scripts.typescript !== undefined ||
|
|
54
|
+
(pkg.devDependencies && pkg.devDependencies.typescript);
|
|
55
|
+
if (!hasTs) {
|
|
56
|
+
console.log('[skip] typecheck: no typecheck script and no TypeScript setup detected');
|
|
57
|
+
return 'skipped';
|
|
58
|
+
}
|
|
59
|
+
console.log('[run] typecheck (fallback: tsc --noEmit — no "typecheck" script was found)');
|
|
60
|
+
const exec = pm === 'bun' ? 'bunx' : pm === 'npm' ? 'npx' : pm;
|
|
61
|
+
const res = run(exec, ['--no-install', 'tsc', '--noEmit']);
|
|
62
|
+
if (res.status !== 0) {
|
|
63
|
+
console.error(`\n[x] typecheck failed (exit ${res.status ?? 'signal'}).`);
|
|
64
|
+
process.exit(res.status ?? 1);
|
|
65
|
+
}
|
|
66
|
+
return 'ran';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`[anti-slop] project checks via ${pm}`);
|
|
70
|
+
runScript('lint');
|
|
71
|
+
runTypecheck();
|
|
72
|
+
runScript('test');
|
|
73
|
+
console.log('[ok] project checks done.');
|
|
@@ -19,4 +19,4 @@ jobs:
|
|
|
19
19
|
- run: node scripts/anti-slop/check-imports.mjs
|
|
20
20
|
- run: node scripts/anti-slop/check-docs.mjs
|
|
21
21
|
- run: node scripts/anti-slop/check-duplicates.mjs
|
|
22
|
-
- run: node scripts/anti-slop/generate-component-index.mjs
|
|
22
|
+
- run: node scripts/anti-slop/generate-component-index.mjs --check
|