@techninja/clearstack 0.4.7 → 0.4.8
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/lib/check.js +21 -60
- package/lib/spec-types.js +54 -0
- package/lib/watch-runner.js +97 -0
- package/lib/watch.js +29 -122
- package/package.json +1 -1
package/lib/check.js
CHANGED
|
@@ -4,24 +4,23 @@
|
|
|
4
4
|
* @module lib/check
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
8
|
import { resolve } from 'node:path';
|
|
9
9
|
import { runCmd } from './spec-utils.js';
|
|
10
10
|
import { countFiles, checkFileLines, checkImports } from './spec-scan.js';
|
|
11
11
|
import { checkI18n } from './spec-i18n.js';
|
|
12
12
|
import { loadConfig, buildCmds, detectRunner } from './spec-config.js';
|
|
13
|
+
import { findTypeConfigs } from './spec-types.js';
|
|
13
14
|
|
|
14
15
|
export { runCmd, elapsed } from './spec-utils.js';
|
|
15
16
|
export { findFiles, countFiles, checkFileLines, checkImports } from './spec-scan.js';
|
|
16
17
|
export { checkI18n } from './spec-i18n.js';
|
|
17
18
|
export { loadConfig, buildCmds, detectRunner } from './spec-config.js';
|
|
18
19
|
|
|
20
|
+
/** @typedef {{ key: string, name: string, parent?: string, watchExts?: string[], fix?: () => void, run: (opts?: object) => (boolean | import('./spec-utils.js').CheckResult | Promise<import('./spec-utils.js').CheckResult>) }} Check */
|
|
21
|
+
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
21
|
-
* The file may export a default array of Check objects. Any check whose key matches
|
|
22
|
-
* a built-in check replaces it; new keys are appended.
|
|
23
|
-
* @param {string} projectDir
|
|
24
|
-
* @returns {Promise<Check[]>}
|
|
23
|
+
*
|
|
25
24
|
*/
|
|
26
25
|
async function loadExtensions(projectDir) {
|
|
27
26
|
const extPath = resolve(projectDir, 'clearstack.spec.js');
|
|
@@ -29,10 +28,7 @@ async function loadExtensions(projectDir) {
|
|
|
29
28
|
try {
|
|
30
29
|
const mod = await import(extPath);
|
|
31
30
|
const exts = mod.default ?? [];
|
|
32
|
-
if (!Array.isArray(exts)) {
|
|
33
|
-
console.warn('⚠ clearstack.spec.js default export must be an array — extensions ignored');
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
31
|
+
if (!Array.isArray(exts)) { console.warn('⚠ clearstack.spec.js default export must be an array — extensions ignored'); return []; }
|
|
36
32
|
return exts;
|
|
37
33
|
} catch (e) {
|
|
38
34
|
console.warn(`⚠ Failed to load clearstack.spec.js: ${e.message}`);
|
|
@@ -40,56 +36,28 @@ async function loadExtensions(projectDir) {
|
|
|
40
36
|
}
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
/** Find all jsconfig.json files — main config + subdirectories. */
|
|
46
|
-
function findTypeConfigs(dir, runner) {
|
|
47
|
-
const main = resolve(dir, '.configs/jsconfig.json');
|
|
48
|
-
const configs = [];
|
|
49
|
-
if (existsSync(main)) configs.push({ key: 'frontend', label: 'Frontend', path: main });
|
|
50
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
51
|
-
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
52
|
-
const p = resolve(dir, entry.name, 'jsconfig.json');
|
|
53
|
-
if (existsSync(p)) configs.push({ key: entry.name, label: entry.name, path: p });
|
|
54
|
-
}
|
|
55
|
-
return configs.map((c) => ({
|
|
56
|
-
key: c.key, name: `JSDoc types — ${c.label}`, parent: 'types',
|
|
57
|
-
run: (o) => runCmd(`Types (${c.label})`, `${runner} tsc --project ${c.path} --noEmit`, dir, undefined, o),
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Build the unified checks array. Children have a `parent` key.
|
|
63
|
-
* Extensions from clearstack.spec.js are merged in: same key = replace, new key = append.
|
|
64
|
-
* @returns {Promise<Check[]>}
|
|
65
|
-
*/
|
|
39
|
+
/** Build the unified checks array. Extensions from clearstack.spec.js are merged in. */
|
|
66
40
|
export async function buildChecks(dir, cfg, cmds) {
|
|
67
41
|
const js = () => countFiles(dir, ['.js'], cfg.ignore);
|
|
68
42
|
const css = () => countFiles(dir, ['.css'], cfg.ignore);
|
|
69
43
|
const md = () => countFiles(dir, ['.md'], cfg.ignore);
|
|
70
44
|
const runner = detectRunner(dir);
|
|
71
45
|
const builtin = [
|
|
72
|
-
{ key: 'es',
|
|
73
|
-
|
|
74
|
-
{ key: '
|
|
75
|
-
|
|
76
|
-
{ key: '
|
|
77
|
-
run: (o) => runCmd('Markdown', cmds.mdlint, dir, `${md()} files`, o) },
|
|
78
|
-
{ key: 'prettier', name: 'Prettier', parent: 'format',
|
|
79
|
-
run: (o) => runCmd('Prettier', cmds.prettier, dir, `${js()} files`, o) },
|
|
80
|
-
{ key: 'lines', name: `Code (max ${cfg.codeMax} lines)`, parent: 'code',
|
|
46
|
+
{ key: 'es', name: 'ESLint', parent: 'lint', run: (o) => runCmd('ESLint', cmds.lint, dir, `${js()} files`, o) },
|
|
47
|
+
{ key: 'css', name: 'Stylelint', parent: 'lint', run: (o) => runCmd('Stylelint', cmds.stylelint, dir, `${css()} files`, o) },
|
|
48
|
+
{ key: 'md', name: 'Markdown lint', parent: 'lint', run: (o) => runCmd('Markdown', cmds.mdlint, dir, `${md()} files`, o) },
|
|
49
|
+
{ key: 'prettier', name: 'Prettier', parent: 'format', run: (o) => runCmd('Prettier', cmds.prettier, dir, `${js()} files`, o) },
|
|
50
|
+
{ key: 'lines', name: `Code (max ${cfg.codeMax} lines)`, parent: 'code',
|
|
81
51
|
run: (o) => checkFileLines(dir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`, { exclude: cfg.testPattern, ...o }) },
|
|
82
|
-
{ key: 'i18n',
|
|
83
|
-
|
|
84
|
-
{ key: 'tests', name: `Tests (max ${cfg.testMax} lines)`,
|
|
52
|
+
{ key: 'i18n', name: 'i18n readiness', parent: 'code', run: (o) => checkI18n(dir, cfg.ignore, 'i18n readiness', o) },
|
|
53
|
+
{ key: 'tests', name: `Tests (max ${cfg.testMax} lines)`,
|
|
85
54
|
run: (o) => checkFileLines(dir, cfg.codeExt, cfg.testMax, cfg.ignore, `Tests (max ${cfg.testMax} lines)`, { include: cfg.testPattern, ...o }) },
|
|
86
|
-
{ key: 'docs',
|
|
55
|
+
{ key: 'docs', name: `Docs (max ${cfg.docsMax} lines)`,
|
|
87
56
|
run: (o) => checkFileLines(dir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`, o) },
|
|
88
57
|
{ key: 'imports', name: 'Import map aliases (no ../ imports)',
|
|
89
58
|
run: (o) => checkImports(dir, cfg.ignore, 'Import map aliases (no ../ imports)', o) },
|
|
90
|
-
...findTypeConfigs(dir, runner),
|
|
91
|
-
{ key: 'audit',
|
|
92
|
-
run: (o) => runCmd('Security audit', cmds.audit, dir, undefined, o) },
|
|
59
|
+
...findTypeConfigs(dir, runner, cfg.ignore),
|
|
60
|
+
{ key: 'audit', name: 'Security audit', run: (o) => runCmd('Security audit', cmds.audit, dir, undefined, o) },
|
|
93
61
|
];
|
|
94
62
|
const extensions = await loadExtensions(dir);
|
|
95
63
|
if (!extensions.length) return builtin;
|
|
@@ -100,10 +68,7 @@ export async function buildChecks(dir, cfg, cmds) {
|
|
|
100
68
|
/** Resolve a scope like 'lint', 'lint es', or 'code' to runnable check(s). */
|
|
101
69
|
export function resolveChecks(checks, scope) {
|
|
102
70
|
const [first, second] = scope.split(/\s+/);
|
|
103
|
-
if (second) {
|
|
104
|
-
const match = checks.find((c) => c.parent === first && c.key === second);
|
|
105
|
-
return match ? [match] : null;
|
|
106
|
-
}
|
|
71
|
+
if (second) { const match = checks.find((c) => c.parent === first && c.key === second); return match ? [match] : null; }
|
|
107
72
|
const children = checks.filter((c) => c.parent === first);
|
|
108
73
|
if (children.length) return children;
|
|
109
74
|
const exact = checks.find((c) => c.key === first && !c.parent);
|
|
@@ -115,12 +80,11 @@ export function parentKeys(checks) {
|
|
|
115
80
|
return [...new Set(checks.filter((c) => c.parent).map((c) => c.parent))];
|
|
116
81
|
}
|
|
117
82
|
|
|
118
|
-
/** Run the full spec compliance check
|
|
83
|
+
/** Run the full spec compliance check. */
|
|
119
84
|
export async function check(projectDir, scope, opts) {
|
|
120
85
|
const cfg = loadConfig(projectDir);
|
|
121
86
|
const cmds = buildCmds(projectDir);
|
|
122
87
|
const checks = await buildChecks(projectDir, cfg, cmds);
|
|
123
|
-
|
|
124
88
|
if (scope && scope !== 'all') {
|
|
125
89
|
const matched = resolveChecks(checks, scope);
|
|
126
90
|
if (!matched) {
|
|
@@ -130,15 +94,12 @@ export async function check(projectDir, scope, opts) {
|
|
|
130
94
|
process.exit(1);
|
|
131
95
|
}
|
|
132
96
|
const results = await Promise.all(matched.map((c) => c.run(opts)));
|
|
133
|
-
|
|
134
|
-
if (!ok) process.exit(1);
|
|
97
|
+
if (!results.every((r) => typeof r === 'boolean' ? r : r.pass)) process.exit(1);
|
|
135
98
|
return;
|
|
136
99
|
}
|
|
137
|
-
|
|
138
100
|
console.log('🔍 Clearstack compliance checking now... 💙\n');
|
|
139
101
|
const results = await Promise.all(checks.map((c) => c.run(opts)));
|
|
140
|
-
const
|
|
141
|
-
const passed = passes.filter(Boolean).length;
|
|
102
|
+
const passed = results.filter((r) => typeof r === 'boolean' ? r : r.pass).length;
|
|
142
103
|
console.log(`\n${'='.repeat(40)}`);
|
|
143
104
|
console.log(`${passed}/${results.length} checks passed.`);
|
|
144
105
|
if (passed < results.length) process.exit(1);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSDoc type check helpers — jsconfig discovery and SPEC_IGNORE_DIRS merging.
|
|
3
|
+
* @module lib/spec-types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { resolve, dirname } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { runCmd } from './spec-utils.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Merge cfg.ignore dirs into a jsconfig's exclude list and write a temp file.
|
|
13
|
+
* Ensures SPEC_IGNORE_DIRS is the single source of truth for all checks.
|
|
14
|
+
* @param {string} configPath
|
|
15
|
+
* @param {string[]} ignore
|
|
16
|
+
* @returns {string} path to temp jsconfig
|
|
17
|
+
*/
|
|
18
|
+
function mergedTypeConfig(configPath, ignore) {
|
|
19
|
+
const base = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
20
|
+
const root = dirname(configPath);
|
|
21
|
+
const existing = base.exclude ?? [];
|
|
22
|
+
const extra = ignore.map((d) => resolve(root, '..', d) + '/**');
|
|
23
|
+
const out = { ...base, exclude: [...new Set([...existing, ...extra])] };
|
|
24
|
+
const tmpDir = resolve(tmpdir(), 'clearstack-types');
|
|
25
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
26
|
+
const tmpPath = resolve(tmpDir, configPath.replace(/[/\\:]/g, '_') + '.json');
|
|
27
|
+
writeFileSync(tmpPath, JSON.stringify(out));
|
|
28
|
+
return tmpPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find all jsconfig.json files and return type check descriptors.
|
|
33
|
+
* @param {string} dir
|
|
34
|
+
* @param {string} runner
|
|
35
|
+
* @param {string[]} ignore
|
|
36
|
+
* @returns {object[]}
|
|
37
|
+
*/
|
|
38
|
+
export function findTypeConfigs(dir, runner, ignore) {
|
|
39
|
+
const main = resolve(dir, '.configs/jsconfig.json');
|
|
40
|
+
const configs = [];
|
|
41
|
+
if (existsSync(main)) configs.push({ key: 'frontend', label: 'Frontend', path: main });
|
|
42
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
43
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
44
|
+
const p = resolve(dir, entry.name, 'jsconfig.json');
|
|
45
|
+
if (existsSync(p)) configs.push({ key: entry.name, label: entry.name, path: p });
|
|
46
|
+
}
|
|
47
|
+
return configs.map((c) => {
|
|
48
|
+
const merged = mergedTypeConfig(c.path, ignore);
|
|
49
|
+
return {
|
|
50
|
+
key: c.key, name: `JSDoc types — ${c.label}`, parent: 'types',
|
|
51
|
+
run: (o) => runCmd(`Types (${c.label})`, `${runner} tsc --project ${merged} --noEmit`, dir, undefined, o),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch runner — check execution, scheduling, and fix dispatch.
|
|
3
|
+
* @module lib/watch-runner
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { FAST_CHECKS } from './spec-config.js';
|
|
8
|
+
import { render, extractViolations } from './watch-ui.js';
|
|
9
|
+
import { setOnFix } from './watch-widgets.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {object[]} specRows
|
|
13
|
+
* @param {Set<string>} keys
|
|
14
|
+
* @param {{ rows: object[], lastCheck: { value: string }, watchDirs: string[], currentViolations: { value: string[][] }, projectDir: string }} ctx
|
|
15
|
+
*/
|
|
16
|
+
export function runKeys(specRows, keys, ctx) {
|
|
17
|
+
const { rows, lastCheck, watchDirs, currentViolations, projectDir } = ctx;
|
|
18
|
+
const toRun = specRows.filter((r) => keys.has(r.key));
|
|
19
|
+
if (!toRun.length) return;
|
|
20
|
+
let i = 0;
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
24
|
+
async function next() {
|
|
25
|
+
if (i >= toRun.length) {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
lastCheck.value = `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
28
|
+
render(rows, lastCheck.value, watchDirs, currentViolations.value);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const row = toRun[i++];
|
|
32
|
+
row.pass = null;
|
|
33
|
+
row.detail = '';
|
|
34
|
+
render(rows, lastCheck.value, watchDirs, currentViolations.value);
|
|
35
|
+
const t0 = Date.now();
|
|
36
|
+
const raw = await row.run({ quiet: true });
|
|
37
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1) + 's';
|
|
38
|
+
row.result = typeof raw === 'boolean' ? { pass: raw } : raw;
|
|
39
|
+
row.pass = row.result.pass;
|
|
40
|
+
row.detail = row.pass
|
|
41
|
+
? `${row.result.detail ?? (row.result.files ? `${row.result.files} files` : '')} (${elapsed})`
|
|
42
|
+
: (row.result.violations?.length
|
|
43
|
+
? `${row.result.violations.length} violation(s) (${elapsed})`
|
|
44
|
+
: (() => { const errs = row.result.errors ?? []; const n = errs.filter((l) => /\.(js|ts|css|md)/.test(l)).length || errs.length; return `${n} error(s) (${elapsed})`; })());
|
|
45
|
+
currentViolations.value = specRows.flatMap((r) => extractViolations(r, projectDir));
|
|
46
|
+
render(rows, lastCheck.value, watchDirs, currentViolations.value);
|
|
47
|
+
setImmediate(next);
|
|
48
|
+
}
|
|
49
|
+
setImmediate(next);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {object[]} specRows
|
|
54
|
+
* @param {object} ctx
|
|
55
|
+
* @param {{ fast: Set<string>, slow: Set<string> }} pending
|
|
56
|
+
* @param {{ fast: ReturnType<typeof setTimeout>|null, slow: ReturnType<typeof setTimeout>|null }} timers
|
|
57
|
+
* @param {(keys: Set<string>) => void} run
|
|
58
|
+
*/
|
|
59
|
+
export function schedule(specRows, ctx, pending, timers, run, checksForExt, ext) {
|
|
60
|
+
for (const k of checksForExt(ext)) {
|
|
61
|
+
const row = specRows.find((r) => r.key === k);
|
|
62
|
+
if (row) row.pass = null;
|
|
63
|
+
if (FAST_CHECKS.has(k)) pending.fast.add(k);
|
|
64
|
+
else pending.slow.add(k);
|
|
65
|
+
}
|
|
66
|
+
render(ctx.rows, ctx.lastCheck.value, ctx.watchDirs, ctx.currentViolations.value);
|
|
67
|
+
if (pending.fast.size) { clearTimeout(timers.fast); timers.fast = setTimeout(() => { const keys = new Set(pending.fast); pending.fast.clear(); run(keys); }, 50); }
|
|
68
|
+
if (pending.slow.size) { clearTimeout(timers.slow); timers.slow = setTimeout(() => { const keys = new Set(pending.slow); pending.slow.clear(); run(keys); }, 1500); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {object[]} specRows
|
|
73
|
+
* @param {object} cmds
|
|
74
|
+
* @param {object} ctx
|
|
75
|
+
* @param {{ slow: Set<string> }} pending
|
|
76
|
+
* @param {{ slow: ReturnType<typeof setTimeout>|null }} timers
|
|
77
|
+
* @param {(keys: Set<string>) => void} run
|
|
78
|
+
*/
|
|
79
|
+
export function setupFix(specRows, cmds, ctx, pending, timers, run) {
|
|
80
|
+
setOnFix(() => {
|
|
81
|
+
const fixable = specRows.filter((r) => !r.pass && (cmds.fix?.[r.key] || r.fix));
|
|
82
|
+
if (!fixable.length) return;
|
|
83
|
+
for (const row of fixable) {
|
|
84
|
+
row.pass = null;
|
|
85
|
+
pending.slow.add(row.key);
|
|
86
|
+
}
|
|
87
|
+
render(ctx.rows, ctx.lastCheck.value, ctx.watchDirs, ctx.currentViolations.value);
|
|
88
|
+
for (const row of fixable) {
|
|
89
|
+
try {
|
|
90
|
+
if (row.fix) row.fix();
|
|
91
|
+
else execSync(cmds.fix[row.key], { cwd: ctx.projectDir, stdio: 'pipe' });
|
|
92
|
+
} catch { /* fixer may exit 1 even after fixing */ }
|
|
93
|
+
}
|
|
94
|
+
clearTimeout(timers.slow);
|
|
95
|
+
timers.slow = setTimeout(() => { const keys = new Set(pending.slow); pending.slow.clear(); run(keys); }, 100);
|
|
96
|
+
});
|
|
97
|
+
}
|
package/lib/watch.js
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spec watch dashboard — continuous compliance monitoring.
|
|
3
|
-
*
|
|
4
|
-
* UI rendering lives in watch-ui.js.
|
|
3
|
+
* Execution logic lives in watch-runner.js, UI in watch-ui.js.
|
|
5
4
|
* @module lib/watch
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { existsSync } from 'node:fs';
|
|
9
8
|
import { resolve } from 'node:path';
|
|
10
|
-
import {
|
|
11
|
-
import { loadConfig, buildCmds, makeExtMap, FAST_CHECKS } from './spec-config.js';
|
|
9
|
+
import { loadConfig, buildCmds, makeExtMap } from './spec-config.js';
|
|
12
10
|
import { buildChecks } from './check.js';
|
|
13
|
-
import { render,
|
|
11
|
+
import { render, renderNote } from './watch-ui.js';
|
|
14
12
|
import { spawnServer, detectServerCmd } from './server-proc.js';
|
|
15
13
|
import { setupLifecycle } from './watch-lifecycle.js';
|
|
16
|
-
import {
|
|
14
|
+
import { runKeys, schedule, setupFix } from './watch-runner.js';
|
|
17
15
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
function toRow(check) { return { ...check, pass: null, detail: '', result: null }; }
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
25
|
-
* @param {string} projectDir
|
|
22
|
+
*
|
|
26
23
|
*/
|
|
27
24
|
export async function startWatch(projectDir) {
|
|
28
25
|
const cfg = loadConfig(projectDir);
|
|
@@ -31,131 +28,41 @@ export async function startWatch(projectDir) {
|
|
|
31
28
|
const specRows = checks.map(toRow);
|
|
32
29
|
const checksForExt = makeExtMap(checks);
|
|
33
30
|
|
|
34
|
-
const watchDirs = ['src', 'scripts', 'docs'].filter((d) =>
|
|
35
|
-
existsSync(resolve(projectDir, d)),
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
let lastCheck = 'never';
|
|
39
|
-
let fastTimer = null;
|
|
40
|
-
let slowTimer = null;
|
|
41
|
-
/** @type {Set<string>} */
|
|
42
|
-
const fastPending = new Set();
|
|
43
|
-
/** @type {Set<string>} */
|
|
44
|
-
const slowPending = new Set();
|
|
31
|
+
const watchDirs = ['src', 'scripts', 'docs'].filter((d) => existsSync(resolve(projectDir, d)));
|
|
45
32
|
|
|
46
|
-
// Render immediately — server spawns and checks run async behind it
|
|
47
33
|
const serverCmd = detectServerCmd(projectDir, cfg);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
const serverRow = serverCmd
|
|
35
|
+
? { key: 'server', label: 'server', status: 'starting', pass: null, detail: 'starting…', kill: () => {} }
|
|
36
|
+
: null;
|
|
51
37
|
const rows = serverRow ? [serverRow, ...specRows] : specRows;
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
// Shared mutable state passed by reference into runner helpers
|
|
40
|
+
const lastCheck = { value: 'never' };
|
|
41
|
+
const currentViolations = { value: [] };
|
|
42
|
+
const pending = { fast: new Set(), slow: new Set() };
|
|
43
|
+
const timers = { fast: null, slow: null };
|
|
44
|
+
const ctx = { rows, lastCheck, watchDirs, currentViolations, projectDir };
|
|
45
|
+
|
|
46
|
+
const run = (keys) => runKeys(specRows, keys, ctx);
|
|
47
|
+
|
|
48
|
+
if (serverCmd) {
|
|
57
49
|
const proc = spawnServer(serverCmd, projectDir, cfg.rawEnv, () => {
|
|
58
50
|
serverRow.status = proc.status;
|
|
59
51
|
serverRow.pass = proc.pass;
|
|
60
52
|
serverRow.detail = proc.detail;
|
|
61
53
|
serverRow.kill = proc.kill;
|
|
62
|
-
render(rows, lastCheck, watchDirs, currentViolations);
|
|
54
|
+
render(rows, lastCheck.value, watchDirs, currentViolations.value);
|
|
63
55
|
});
|
|
64
56
|
}
|
|
65
|
-
if (serverCmd) spawnAndWatch();
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
render(rows, lastCheck, watchDirs, currentViolations);
|
|
58
|
+
render(rows, lastCheck.value, watchDirs, currentViolations.value);
|
|
59
|
+
run(new Set(specRows.map((r) => r.key)));
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
*/
|
|
74
|
-
function runKeys(keys) {
|
|
75
|
-
const toRun = specRows.filter((r) => keys.has(r.key));
|
|
76
|
-
if (!toRun.length) return;
|
|
77
|
-
let i = 0;
|
|
78
|
-
/**
|
|
79
|
-
*
|
|
80
|
-
*/
|
|
81
|
-
async function next() {
|
|
82
|
-
if (i >= toRun.length) {
|
|
83
|
-
const now = new Date();
|
|
84
|
-
lastCheck = `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
85
|
-
render(rows, lastCheck, watchDirs, currentViolations);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const row = toRun[i++];
|
|
89
|
-
row.pass = null;
|
|
90
|
-
row.detail = '';
|
|
91
|
-
render(rows, lastCheck, watchDirs, currentViolations);
|
|
92
|
-
const t0 = Date.now();
|
|
93
|
-
const raw = await row.run({ quiet: true });
|
|
94
|
-
const elapsed = ((Date.now() - t0) / 1000).toFixed(1) + 's';
|
|
95
|
-
row.result = typeof raw === 'boolean' ? { pass: raw } : raw;
|
|
96
|
-
row.pass = row.result.pass;
|
|
97
|
-
row.detail = row.pass
|
|
98
|
-
? `${row.result.detail ?? (row.result.files ? `${row.result.files} files` : '')} (${elapsed})`
|
|
99
|
-
: (row.result.violations?.length
|
|
100
|
-
? `${row.result.violations.length} violation(s) (${elapsed})`
|
|
101
|
-
: (() => { const errs = row.result.errors ?? []; const n = errs.filter((l) => /\.(js|ts|css|md)/.test(l)).length || errs.length; return `${n} error(s) (${elapsed})`; })());
|
|
102
|
-
// Rebuild violations from all currently-failed rows
|
|
103
|
-
currentViolations = specRows.flatMap((r) => extractViolations(r, projectDir));
|
|
104
|
-
render(rows, lastCheck, watchDirs, currentViolations);
|
|
105
|
-
setImmediate(next);
|
|
106
|
-
}
|
|
107
|
-
setImmediate(next);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
*
|
|
112
|
-
*/
|
|
113
|
-
function runFast() { const keys = new Set(fastPending); fastPending.clear(); runKeys(keys); }
|
|
114
|
-
/**
|
|
115
|
-
*
|
|
116
|
-
*/
|
|
117
|
-
function runSlow() { const keys = new Set(slowPending); slowPending.clear(); runKeys(keys); }
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
*
|
|
121
|
-
*/
|
|
122
|
-
function schedule(ext) {
|
|
123
|
-
for (const k of checksForExt(ext)) {
|
|
124
|
-
const row = specRows.find((r) => r.key === k);
|
|
125
|
-
if (row) row.pass = null; // show spinner while queued
|
|
126
|
-
if (FAST_CHECKS.has(k)) fastPending.add(k);
|
|
127
|
-
else slowPending.add(k);
|
|
128
|
-
}
|
|
129
|
-
render(rows, lastCheck, watchDirs, currentViolations);
|
|
130
|
-
if (fastPending.size) { clearTimeout(fastTimer); fastTimer = setTimeout(runFast, 50); }
|
|
131
|
-
if (slowPending.size) { clearTimeout(slowTimer); slowTimer = setTimeout(runSlow, 1500); }
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
runKeys(new Set(specRows.map((r) => r.key)));
|
|
135
|
-
|
|
136
|
-
// f key — run fix commands for all currently-failing fixable checks
|
|
137
|
-
// Checks can provide a fix via cmds.fix[key] (built-ins) or row.fix() (extensions)
|
|
138
|
-
setOnFix(() => {
|
|
139
|
-
const fixable = specRows.filter((r) => !r.pass && (cmds.fix?.[r.key] || r.fix));
|
|
140
|
-
if (!fixable.length) return;
|
|
141
|
-
for (const row of fixable) {
|
|
142
|
-
row.pass = null; // show spinner immediately
|
|
143
|
-
slowPending.add(row.key);
|
|
144
|
-
}
|
|
145
|
-
render(rows, lastCheck, watchDirs, currentViolations);
|
|
146
|
-
for (const row of fixable) {
|
|
147
|
-
try {
|
|
148
|
-
if (row.fix) row.fix();
|
|
149
|
-
else execSync(cmds.fix[row.key], { cwd: projectDir, stdio: 'pipe' });
|
|
150
|
-
} catch { /* fixer may exit 1 even after fixing */ }
|
|
151
|
-
}
|
|
152
|
-
clearTimeout(slowTimer);
|
|
153
|
-
slowTimer = setTimeout(runSlow, 100);
|
|
154
|
-
});
|
|
61
|
+
setupFix(specRows, cmds, ctx, pending, timers, run);
|
|
155
62
|
|
|
156
63
|
setupLifecycle({
|
|
157
64
|
serverRow, specRows, projectDir, watchDirs,
|
|
158
|
-
schedule,
|
|
159
|
-
renderNote: () => renderNote(rows, lastCheck, watchDirs),
|
|
65
|
+
schedule: (ext) => schedule(specRows, ctx, pending, timers, run, checksForExt, ext),
|
|
66
|
+
renderNote: () => renderNote(rows, lastCheck.value, watchDirs),
|
|
160
67
|
});
|
|
161
68
|
}
|