@techninja/clearstack 0.2.18 → 0.2.19
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 +42 -79
- package/lib/spec-utils.js +109 -0
- package/package.json +1 -1
- package/templates/shared/src/styles/shared.css +3 -3
package/lib/check.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Spec compliance checker —
|
|
3
|
-
*
|
|
2
|
+
* Spec compliance checker — config, commands, and orchestration.
|
|
3
|
+
* Core utilities live in spec-utils.js.
|
|
4
4
|
* @module lib/check
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { readFileSync,
|
|
8
|
-
import { resolve
|
|
9
|
-
import {
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { checkFileLines, runCmd, countFiles } from './spec-utils.js';
|
|
10
|
+
|
|
11
|
+
export { checkFileLines, runCmd, countFiles, findFiles, elapsed } from './spec-utils.js';
|
|
12
|
+
|
|
13
|
+
/** @param {string} src */
|
|
14
|
+
function parseEnv(src) {
|
|
15
|
+
const env = {};
|
|
16
|
+
for (const line of src.split('\n')) {
|
|
17
|
+
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
18
|
+
if (m) env[m[1].trim()] = m[2].trim();
|
|
19
|
+
}
|
|
20
|
+
return env;
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Load spec config from project .env.
|
|
13
25
|
* @param {string} projectDir
|
|
14
26
|
*/
|
|
15
|
-
function loadConfig(projectDir) {
|
|
27
|
+
export function loadConfig(projectDir) {
|
|
16
28
|
const envPath = resolve(projectDir, '.env');
|
|
17
29
|
const env = existsSync(envPath) ? parseEnv(readFileSync(envPath, 'utf-8')) : {};
|
|
18
30
|
return {
|
|
@@ -24,34 +36,47 @@ function loadConfig(projectDir) {
|
|
|
24
36
|
};
|
|
25
37
|
}
|
|
26
38
|
|
|
39
|
+
/** Standard check commands used by both CLI and POC. */
|
|
40
|
+
export const CMDS = {
|
|
41
|
+
lint: 'npx eslint --config .configs/eslint.config.js . --fix',
|
|
42
|
+
stylelint: 'npx stylelint --config .configs/.stylelintrc.json "src/**/*.css" --fix',
|
|
43
|
+
prettier: 'npx prettier --config .configs/.prettierrc --write src scripts',
|
|
44
|
+
mdlint: 'npx markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"',
|
|
45
|
+
types: 'npx tsc --project .configs/jsconfig.json',
|
|
46
|
+
};
|
|
47
|
+
|
|
27
48
|
/**
|
|
28
|
-
* Run spec compliance
|
|
49
|
+
* Run the full spec compliance check (used by clearstack CLI).
|
|
29
50
|
* @param {string} projectDir
|
|
30
|
-
* @param {string} [scope]
|
|
51
|
+
* @param {string} [scope]
|
|
31
52
|
*/
|
|
32
53
|
export async function check(projectDir, scope) {
|
|
33
54
|
const cfg = loadConfig(projectDir);
|
|
34
55
|
|
|
35
56
|
if (scope === 'code') {
|
|
36
|
-
if (!
|
|
57
|
+
if (!checkFileLines(projectDir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`))
|
|
37
58
|
process.exit(1);
|
|
38
59
|
return;
|
|
39
60
|
}
|
|
40
61
|
if (scope === 'docs') {
|
|
41
|
-
if (!
|
|
62
|
+
if (!checkFileLines(projectDir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`))
|
|
42
63
|
process.exit(1);
|
|
43
64
|
return;
|
|
44
65
|
}
|
|
45
66
|
|
|
67
|
+
const jsFiles = countFiles(projectDir, ['.js'], cfg.ignore);
|
|
68
|
+
const cssFiles = countFiles(projectDir, ['.css'], cfg.ignore);
|
|
69
|
+
const mdFiles = countFiles(projectDir, ['.md'], cfg.ignore);
|
|
70
|
+
|
|
46
71
|
console.log('Running spec compliance check...\n');
|
|
47
72
|
const results = [
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
runCmd('ESLint',
|
|
51
|
-
runCmd('Stylelint',
|
|
52
|
-
runCmd('Prettier',
|
|
53
|
-
runCmd('Markdown',
|
|
54
|
-
runCmd('JSDoc types',
|
|
73
|
+
checkFileLines(projectDir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`),
|
|
74
|
+
checkFileLines(projectDir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`),
|
|
75
|
+
runCmd('ESLint', CMDS.lint, projectDir, `${jsFiles} files`),
|
|
76
|
+
runCmd('Stylelint', CMDS.stylelint, projectDir, `${cssFiles} files`),
|
|
77
|
+
runCmd('Prettier', CMDS.prettier, projectDir, `${jsFiles} files`),
|
|
78
|
+
runCmd('Markdown', CMDS.mdlint, projectDir, `${mdFiles} files`),
|
|
79
|
+
runCmd('JSDoc types', CMDS.types, projectDir, `${jsFiles} files`),
|
|
55
80
|
];
|
|
56
81
|
|
|
57
82
|
const passed = results.filter(Boolean).length;
|
|
@@ -59,65 +84,3 @@ export async function check(projectDir, scope) {
|
|
|
59
84
|
console.log(`${passed}/${results.length} checks passed.`);
|
|
60
85
|
if (passed < results.length) process.exit(1);
|
|
61
86
|
}
|
|
62
|
-
|
|
63
|
-
/** @param {string} src */
|
|
64
|
-
function parseEnv(src) {
|
|
65
|
-
const env = {};
|
|
66
|
-
for (const line of src.split('\n')) {
|
|
67
|
-
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
68
|
-
if (m) env[m[1].trim()] = m[2].trim();
|
|
69
|
-
}
|
|
70
|
-
return env;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Check file line counts. */
|
|
74
|
-
function checkFiles(root, extensions, max, ignoreDirs, label) {
|
|
75
|
-
const violations = [];
|
|
76
|
-
findFiles(root, extensions, ignoreDirs, root).forEach((file) => {
|
|
77
|
-
const lines = readFileSync(resolve(root, file), 'utf-8').trimEnd().split('\n').length;
|
|
78
|
-
if (lines > max) violations.push({ file, lines, max });
|
|
79
|
-
});
|
|
80
|
-
if (violations.length === 0) {
|
|
81
|
-
console.log(` ✅ ${label}`);
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
console.log(` ❌ ${label} — ${violations.length} violation(s):`);
|
|
85
|
-
violations.forEach((v) => console.log(` ${v.file}: ${v.lines} lines (max ${v.max})`));
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Recursively find files. */
|
|
90
|
-
function findFiles(dir, extensions, ignoreDirs, root) {
|
|
91
|
-
const results = [];
|
|
92
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
-
const full = resolve(dir, entry.name);
|
|
94
|
-
const rel = relative(root, full);
|
|
95
|
-
if (entry.isDirectory()) {
|
|
96
|
-
if (ignoreDirs.some((ig) => entry.name === ig || rel === ig || rel.startsWith(ig + '/'))) continue;
|
|
97
|
-
results.push(...findFiles(full, extensions, ignoreDirs, root));
|
|
98
|
-
} else if (extensions.includes(extname(entry.name))) {
|
|
99
|
-
results.push(rel);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return results;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Run a shell command, report pass/fail. Filters node_modules errors. */
|
|
106
|
-
function runCmd(label, cmd, cwd) {
|
|
107
|
-
try {
|
|
108
|
-
execSync(cmd, { cwd, stdio: 'pipe' });
|
|
109
|
-
console.log(` ✅ ${label}`);
|
|
110
|
-
return true;
|
|
111
|
-
} catch (err) {
|
|
112
|
-
const out = (err.stdout || '') + (err.stderr || '');
|
|
113
|
-
const ownErrors = out.trim().split('\n')
|
|
114
|
-
.filter((l) => l.trim() && !l.includes('node_modules'));
|
|
115
|
-
if (ownErrors.length === 0) {
|
|
116
|
-
console.log(` ✅ ${label}`);
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
console.log(` ❌ ${label}`);
|
|
120
|
-
ownErrors.forEach((l) => console.log(` ${l}`));
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared spec utilities — file scanning, command running, timing.
|
|
3
|
+
* Used by both the clearstack CLI and the POC spec runner.
|
|
4
|
+
* @module lib/spec-utils
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
8
|
+
import { resolve, extname, relative } from 'node:path';
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
/** @param {number} start */
|
|
12
|
+
export function elapsed(start) {
|
|
13
|
+
const d = performance.now() - start;
|
|
14
|
+
return d < 1000 ? `${Math.round(d)}ms` : `${(d / 1000).toFixed(1)}s`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Recursively find files matching extensions, skipping ignored dirs. */
|
|
18
|
+
export function findFiles(dir, extensions, ignoreDirs, root = dir) {
|
|
19
|
+
const results = [];
|
|
20
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
+
const full = resolve(dir, entry.name);
|
|
22
|
+
const rel = relative(root, full);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
if (ignoreDirs.some((ig) => entry.name === ig || rel === ig || rel.startsWith(ig + '/'))) continue;
|
|
25
|
+
results.push(...findFiles(full, extensions, ignoreDirs, root));
|
|
26
|
+
} else if (extensions.includes(extname(entry.name))) {
|
|
27
|
+
results.push(rel);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Count files matching extensions.
|
|
35
|
+
* @param {string} root
|
|
36
|
+
* @param {string[]} extensions
|
|
37
|
+
* @param {string[]} ignoreDirs
|
|
38
|
+
* @param {string[]} [dirs] - Subdirectories to scope to
|
|
39
|
+
* @param {RegExp} [filter] - Optional regex filter on filenames
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
export function countFiles(root, extensions, ignoreDirs, dirs, filter) {
|
|
43
|
+
let files;
|
|
44
|
+
if (dirs) {
|
|
45
|
+
files = dirs.reduce((acc, d) => {
|
|
46
|
+
try { return acc.concat(findFiles(resolve(root, d), extensions, ignoreDirs, root)); }
|
|
47
|
+
catch { return acc; }
|
|
48
|
+
}, []);
|
|
49
|
+
} else {
|
|
50
|
+
files = findFiles(root, extensions, ignoreDirs, root);
|
|
51
|
+
}
|
|
52
|
+
return filter ? files.filter((f) => filter.test(f)).length : files.length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check file line counts with timing.
|
|
57
|
+
* @param {string} root
|
|
58
|
+
* @param {string[]} extensions
|
|
59
|
+
* @param {number} max
|
|
60
|
+
* @param {string[]} ignoreDirs
|
|
61
|
+
* @param {string} label
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
export function checkFileLines(root, extensions, max, ignoreDirs, label) {
|
|
65
|
+
const start = performance.now();
|
|
66
|
+
const files = findFiles(root, extensions, ignoreDirs, root);
|
|
67
|
+
const violations = [];
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const lines = readFileSync(resolve(root, file), 'utf-8').trimEnd().split('\n').length;
|
|
70
|
+
if (lines > max) violations.push({ file, lines, max });
|
|
71
|
+
}
|
|
72
|
+
const time = elapsed(start);
|
|
73
|
+
if (violations.length === 0) {
|
|
74
|
+
console.log(` ✅ ${label} (${files.length} files, ${time})`);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
console.log(` ❌ ${label} — ${violations.length} violation(s):`);
|
|
78
|
+
for (const v of violations) console.log(` ${v.file}: ${v.lines} lines (max ${v.max})`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Run a shell command, report pass/fail with timing.
|
|
84
|
+
* @param {string} label
|
|
85
|
+
* @param {string} cmd
|
|
86
|
+
* @param {string} cwd
|
|
87
|
+
* @param {string} [stats]
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
export function runCmd(label, cmd, cwd, stats) {
|
|
91
|
+
const start = performance.now();
|
|
92
|
+
const suffix = (s) => s ? ` (${s}, ${elapsed(start)})` : ` (${elapsed(start)})`;
|
|
93
|
+
try {
|
|
94
|
+
execSync(cmd, { cwd, stdio: 'pipe', encoding: 'utf-8' });
|
|
95
|
+
console.log(` ✅ ${label}${suffix(stats)}`);
|
|
96
|
+
return true;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const out = (err.stdout || '') + (err.stderr || '');
|
|
99
|
+
const ownErrors = out.trim().split('\n')
|
|
100
|
+
.filter((l) => l.trim() && !l.includes('node_modules'));
|
|
101
|
+
if (ownErrors.length === 0) {
|
|
102
|
+
console.log(` ✅ ${label}${suffix(stats)}`);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
console.log(` ❌ ${label}${suffix(stats)}`);
|
|
106
|
+
for (const line of ownErrors) console.log(` ${line}`);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
padding: 0;
|
|
85
85
|
margin: -1px;
|
|
86
86
|
overflow: hidden;
|
|
87
|
-
clip:
|
|
87
|
+
clip-path: inset(50%);
|
|
88
88
|
white-space: nowrap;
|
|
89
89
|
border: 0;
|
|
90
90
|
}
|
|
@@ -92,10 +92,10 @@
|
|
|
92
92
|
/* --- Common Transitions --- */
|
|
93
93
|
|
|
94
94
|
.fade-in {
|
|
95
|
-
animation:
|
|
95
|
+
animation: fade-in 0.2s ease-in;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
@keyframes
|
|
98
|
+
@keyframes fade-in {
|
|
99
99
|
from {
|
|
100
100
|
opacity: 0;
|
|
101
101
|
}
|