@techninja/clearstack 0.2.18 → 0.2.20

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 CHANGED
@@ -1,18 +1,30 @@
1
1
  /**
2
- * Spec compliance checker — validates line counts, lint, format, types, tests.
3
- * Reads thresholds from the project's .env file.
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, readdirSync, existsSync } from 'node:fs';
8
- import { resolve, extname, relative } from 'node:path';
9
- import { execSync } from 'node:child_process';
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 checks on a project directory.
49
+ * Run the full spec compliance check (used by clearstack CLI).
29
50
  * @param {string} projectDir
30
- * @param {string} [scope] - 'code', 'docs', or undefined for full check
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 (!checkFiles(projectDir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`))
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 (!checkFiles(projectDir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`))
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
- checkFiles(projectDir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`),
49
- checkFiles(projectDir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`),
50
- runCmd('ESLint', 'npx eslint --config .configs/eslint.config.js . --fix', projectDir),
51
- runCmd('Stylelint', 'npx stylelint --config .configs/.stylelintrc.json "src/**/*.css" --fix', projectDir),
52
- runCmd('Prettier', 'npx prettier --config .configs/.prettierrc --check src', projectDir),
53
- runCmd('Markdown', 'npx markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"', projectDir),
54
- runCmd('JSDoc types', 'npx tsc --project .configs/jsconfig.json', projectDir),
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
- }
@@ -17,21 +17,18 @@ export async function writePackageJson(dest, vars, existing) {
17
17
  const isFullstack = vars.mode === 'fullstack';
18
18
 
19
19
  const specScripts = {
20
- ...(isFullstack ? {
21
- start: 'node src/server.js',
22
- dev: 'node --watch --env-file=.env src/server.js',
23
- } : {
24
- dev: 'npx serve src',
25
- }),
20
+ start: 'node src/server.js',
21
+ dev: 'node --watch --env-file=.env src/server.js',
26
22
  postinstall: 'node scripts/setup.js',
27
23
  test: 'node scripts/test.js',
28
24
  spec: 'clearstack',
29
25
  };
30
26
 
31
27
  const specDeps = {
28
+ express: '^5.2.1',
32
29
  hybrids: '^9.1.22',
33
30
  'lucide-static': '^1.7.0',
34
- ...(isFullstack ? { express: '^5.2.1', ws: '^8.0.0' } : {}),
31
+ ...(isFullstack ? { ws: '^8.0.0' } : {}),
35
32
  };
36
33
 
37
34
  const specDevDeps = {
@@ -57,7 +54,7 @@ export async function writePackageJson(dest, vars, existing) {
57
54
  name: vars.name,
58
55
  description: vars.description,
59
56
  type: 'module',
60
- ...(isFullstack ? { main: 'src/server.js' } : {}),
57
+ main: 'src/server.js',
61
58
  scripts: { ...existing.scripts, ...specScripts },
62
59
  dependencies: { ...existing.dependencies, ...specDeps },
63
60
  devDependencies: { ...existing.devDependencies, ...specDevDeps },
@@ -67,7 +64,7 @@ export async function writePackageJson(dest, vars, existing) {
67
64
  version: '0.1.0',
68
65
  type: 'module',
69
66
  description: vars.description,
70
- ...(isFullstack ? { main: 'src/server.js' } : {}),
67
+ main: 'src/server.js',
71
68
  scripts: specScripts,
72
69
  keywords: ['hybrids', 'web-components', 'no-build'],
73
70
  license: 'MIT',
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -84,7 +84,7 @@
84
84
  padding: 0;
85
85
  margin: -1px;
86
86
  overflow: hidden;
87
- clip: rect(0, 0, 0, 0);
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: fadeIn 0.2s ease-in;
95
+ animation: fade-in 0.2s ease-in;
96
96
  }
97
97
 
98
- @keyframes fadeIn {
98
+ @keyframes fade-in {
99
99
  from {
100
100
  opacity: 0;
101
101
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Static dev server — serves src/ for the browser with SPA fallback.
3
+ * @module server
4
+ */
5
+
6
+ import express from 'express';
7
+
8
+ /** @type {any} */
9
+ const app = express();
10
+
11
+ app.use(express.static('src'));
12
+
13
+ app.use((req, res, next) => {
14
+ if (req.method === 'GET' && !req.path.includes('.')) {
15
+ return res.sendFile('index.html', { root: 'src/public' });
16
+ }
17
+ next();
18
+ });
19
+
20
+ /** @param {number} [port] */
21
+ export function start(port = {{port}}) {
22
+ const server = app.listen(port, () => console.log(`http://localhost:${port}`));
23
+ return server;
24
+ }
25
+
26
+ if (import.meta.url === `file://${process.argv[1]}`) {
27
+ start(parseInt(process.env.PORT) || {{port}});
28
+ }
29
+
30
+ export default app;