@techninja/clearstack 0.2.6 → 0.2.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.
@@ -213,6 +213,50 @@ function moveObj(o, dx, dy) { ... }
213
213
 
214
214
  ---
215
215
 
216
+ ## npm Scripts: One Entry Point Per Domain
217
+
218
+ Every `package.json` script should be a single, discoverable entry point.
219
+ Avoid the `name:variant` colon pattern that fragments a domain across
220
+ multiple keys.
221
+
222
+ ### Rules
223
+
224
+ - **One script per domain.** `test`, `spec`, `lint` — not `test:node`,
225
+ `test:browser`, `lint:fix`, `spec:code`, `spec:docs`.
226
+ - **Arguments over aliases.** `pnpm spec check code` instead of
227
+ `pnpm spec:code`. The CLI handles routing.
228
+ - **Interactive by default.** Running `pnpm spec` with no arguments shows
229
+ a menu of available actions. Users discover commands by using the tool.
230
+ - **Direct invocation for power users.** Once you know the subcommand,
231
+ skip the menu: `pnpm spec check`, `pnpm spec update`.
232
+ - **Self-documenting.** Each script's CLI should print usage when given
233
+ `help` or an unknown argument.
234
+
235
+ ### Why
236
+
237
+ - Fewer script entries = less package.json bloat
238
+ - Discoverability through interactive menus beats memorizing key names
239
+ - Scripts grow via subcommands, not new `package.json` entries
240
+ - Consistent with how real CLIs work (`git`, `docker`, `npm` itself)
241
+
242
+ ### Example
243
+
244
+ ```json
245
+ {
246
+ "scripts": {
247
+ "start": "node src/server.js",
248
+ "dev": "node --watch --env-file=.env src/server.js",
249
+ "test": "node --test tests/*.test.js",
250
+ "spec": "clearstack"
251
+ }
252
+ }
253
+ ```
254
+
255
+ `pnpm spec` → interactive menu. `pnpm spec check` → run checks.
256
+ `pnpm spec update` → sync docs. One entry, full access.
257
+
258
+ ---
259
+
216
260
  ## Session Retrospective
217
261
 
218
262
  At the end of each implementation session, ask:
package/lib/check.js CHANGED
@@ -20,7 +20,7 @@ function loadConfig(projectDir) {
20
20
  docsMax: parseInt(env.SPEC_DOCS_MAX_LINES) || 500,
21
21
  codeExt: (env.SPEC_CODE_EXTENSIONS || '.js,.css').split(','),
22
22
  docsExt: (env.SPEC_DOCS_EXTENSIONS || '.md').split(','),
23
- ignore: (env.SPEC_IGNORE_DIRS || 'node_modules,public/vendor,.git,.configs').split(','),
23
+ ignore: (env.SPEC_IGNORE_DIRS || 'node_modules,src/public/vendor,.git,.configs').split(','),
24
24
  };
25
25
  }
26
26
 
@@ -100,16 +100,22 @@ function findFiles(dir, extensions, ignoreDirs, root) {
100
100
  return results;
101
101
  }
102
102
 
103
- /** Run a shell command, report pass/fail. */
103
+ /** Run a shell command, report pass/fail. Filters node_modules errors. */
104
104
  function runCmd(label, cmd, cwd) {
105
105
  try {
106
106
  execSync(cmd, { cwd, stdio: 'pipe' });
107
107
  console.log(` ✅ ${label}`);
108
108
  return true;
109
109
  } catch (err) {
110
- console.log(` ❌ ${label}`);
111
110
  const out = (err.stdout || '') + (err.stderr || '');
112
- if (out.trim()) out.trim().split('\n').forEach((l) => console.log(` ${l}`));
111
+ const ownErrors = out.trim().split('\n')
112
+ .filter((l) => l.trim() && !l.includes('node_modules'));
113
+ if (ownErrors.length === 0) {
114
+ console.log(` ✅ ${label}`);
115
+ return true;
116
+ }
117
+ console.log(` ❌ ${label}`);
118
+ ownErrors.forEach((l) => console.log(` ${l}`));
113
119
  return false;
114
120
  }
115
121
  }
@@ -20,19 +20,12 @@ export async function writePackageJson(dest, vars, existing) {
20
20
  ...(isFullstack ? {
21
21
  start: 'node src/server.js',
22
22
  dev: 'node --watch --env-file=.env src/server.js',
23
- } : {}),
24
- postinstall: 'node scripts/vendor-deps.js && node scripts/build-icons.js',
25
- test: 'npm run test:node && npm run test:browser',
26
- 'test:node': 'node --test tests/*.test.js src/utils/*.test.js src/store/*.test.js',
27
- 'test:browser': 'web-test-runner --config .configs/web-test-runner.config.js',
28
- spec: 'clearstack check',
29
- 'spec:code': 'clearstack check code',
30
- 'spec:docs': 'clearstack check docs',
31
- 'spec:update': 'clearstack update',
32
- lint: 'eslint --config .configs/eslint.config.js .',
33
- 'lint:fix': 'eslint --config .configs/eslint.config.js . --fix',
34
- format: 'prettier --config .configs/.prettierrc --write src scripts tests',
35
- typecheck: 'tsc --project .configs/jsconfig.json',
23
+ } : {
24
+ dev: 'npx serve src',
25
+ }),
26
+ postinstall: 'node scripts/setup.js',
27
+ test: 'node scripts/test.js',
28
+ spec: 'clearstack',
36
29
  };
37
30
 
38
31
  const specDeps = {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
7
- "clearstack": "./bin/cli.js"
7
+ "clearstack": "bin/cli.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/",
@@ -20,24 +20,17 @@
20
20
  "start": "node src/server.js",
21
21
  "dev": "node --watch --env-file=.env src/server.js",
22
22
  "setup": "node scripts/vendor-deps.js && node scripts/build-icons.js",
23
- "test": "npm run test:node && npm run test:browser",
24
- "test:node": "node --test tests/*.test.js src/utils/*.test.js src/store/*.test.js",
25
- "test:browser": "web-test-runner --config .configs/web-test-runner.config.js",
26
- "test:watch": "web-test-runner --config .configs/web-test-runner.config.js --watch",
23
+ "test": "node --test tests/*.test.js src/utils/*.test.js src/store/*.test.js",
27
24
  "spec": "node --env-file=.env scripts/spec.js",
28
- "spec:code": "node --env-file=.env scripts/spec.js code",
29
- "spec:docs": "node --env-file=.env scripts/spec.js docs",
30
- "lint": "eslint --config .configs/eslint.config.js .",
31
- "lint:fix": "eslint --config .configs/eslint.config.js . --fix",
25
+ "lint": "eslint --config .configs/eslint.config.js . --fix",
32
26
  "format": "prettier --config .configs/.prettierrc --write src scripts tests",
33
- "format:check": "prettier --config .configs/.prettierrc --check src scripts tests",
34
27
  "typecheck": "tsc --project .configs/jsconfig.json",
35
- "sync-docs": "node scripts/sync-docs.js",
36
28
  "release": "node scripts/release.js",
37
- "release:minor": "node scripts/release.js minor",
38
- "release:major": "node scripts/release.js major",
39
29
  "prepublishOnly": "node scripts/sync-docs.js"
40
30
  },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
41
34
  "keywords": [
42
35
  "web-components",
43
36
  "no-build",
@@ -10,16 +10,16 @@ import { eventsRouter } from './api/events.js';
10
10
  const app = express();
11
11
 
12
12
  app.use(express.json());
13
- app.use(express.static('public'));
14
- app.use('/src', express.static('src'));
15
13
 
16
14
  app.use('/api', eventsRouter);
17
15
  app.use('/api', entityRouter);
18
16
 
17
+ app.use(express.static('src'));
18
+
19
19
  // SPA fallback
20
20
  app.use((req, res, next) => {
21
- if (req.method === 'GET' && !req.path.includes('.')) {
22
- return res.sendFile('index.html', { root: 'public' });
21
+ if (req.method === 'GET' && !req.path.includes('.') && !req.path.startsWith('/api')) {
22
+ return res.sendFile('index.html', { root: 'src/public' });
23
23
  }
24
24
  next();
25
25
  });
@@ -58,7 +58,7 @@ export default [
58
58
  },
59
59
  },
60
60
  {
61
- ignores: ['node_modules/', 'public/vendor/'],
61
+ ignores: ['node_modules/', 'src/public/vendor/'],
62
62
  },
63
63
  prettier,
64
64
  ];
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "exclude": [
21
21
  "../node_modules",
22
- "../public/vendor",
22
+ "../src/public/vendor",
23
23
  "../**/*.test.js"
24
24
  ]
25
25
  }
@@ -3,7 +3,7 @@ SPEC_CODE_MAX_LINES=150
3
3
  SPEC_DOCS_MAX_LINES=500
4
4
  SPEC_CODE_EXTENSIONS=.js,.css
5
5
  SPEC_DOCS_EXTENSIONS=.md
6
- SPEC_IGNORE_DIRS=node_modules,public/vendor,.git,.configs
6
+ SPEC_IGNORE_DIRS=node_modules,src/public/vendor,.git,.configs
7
7
 
8
8
  # Server
9
9
  PORT={{port}}
@@ -213,6 +213,50 @@ function moveObj(o, dx, dy) { ... }
213
213
 
214
214
  ---
215
215
 
216
+ ## npm Scripts: One Entry Point Per Domain
217
+
218
+ Every `package.json` script should be a single, discoverable entry point.
219
+ Avoid the `name:variant` colon pattern that fragments a domain across
220
+ multiple keys.
221
+
222
+ ### Rules
223
+
224
+ - **One script per domain.** `test`, `spec`, `lint` — not `test:node`,
225
+ `test:browser`, `lint:fix`, `spec:code`, `spec:docs`.
226
+ - **Arguments over aliases.** `pnpm spec check code` instead of
227
+ `pnpm spec:code`. The CLI handles routing.
228
+ - **Interactive by default.** Running `pnpm spec` with no arguments shows
229
+ a menu of available actions. Users discover commands by using the tool.
230
+ - **Direct invocation for power users.** Once you know the subcommand,
231
+ skip the menu: `pnpm spec check`, `pnpm spec update`.
232
+ - **Self-documenting.** Each script's CLI should print usage when given
233
+ `help` or an unknown argument.
234
+
235
+ ### Why
236
+
237
+ - Fewer script entries = less package.json bloat
238
+ - Discoverability through interactive menus beats memorizing key names
239
+ - Scripts grow via subcommands, not new `package.json` entries
240
+ - Consistent with how real CLIs work (`git`, `docker`, `npm` itself)
241
+
242
+ ### Example
243
+
244
+ ```json
245
+ {
246
+ "scripts": {
247
+ "start": "node src/server.js",
248
+ "dev": "node --watch --env-file=.env src/server.js",
249
+ "test": "node --test tests/*.test.js",
250
+ "spec": "clearstack"
251
+ }
252
+ }
253
+ ```
254
+
255
+ `pnpm spec` → interactive menu. `pnpm spec check` → run checks.
256
+ `pnpm spec update` → sync docs. One entry, full access.
257
+
258
+ ---
259
+
216
260
  ## Session Retrospective
217
261
 
218
262
  At the end of each implementation session, ask:
@@ -1,5 +1,5 @@
1
1
  node_modules/
2
- public/vendor/
3
- public/icons.json
2
+ src/public/vendor/
3
+ src/public/icons.json
4
4
  data/db.json
5
5
  .env.local
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
12
12
 
13
13
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
14
14
  const ICONS_DIR = resolve(ROOT, 'node_modules/lucide-static/icons');
15
- const OUT = resolve(ROOT, 'public/icons.json');
15
+ const OUT = resolve(ROOT, 'src/public/icons.json');
16
16
 
17
17
  /** Icons used in the app — lucide name → app name */
18
18
  const ICON_MAP = {
@@ -83,4 +83,4 @@ for (const [lucideName, appName] of Object.entries(ICON_MAP)) {
83
83
  }
84
84
 
85
85
  writeFileSync(OUT, JSON.stringify(icons, null, 2));
86
- console.log(`✓ Built ${count} icons → public/icons.json`);
86
+ console.log(`✓ Built ${count} icons → src/public/icons.json`);
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install setup — vendors dependencies and builds icon sprite.
5
+ * @module scripts/setup
6
+ */
7
+
8
+ import { dirname, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
12
+
13
+ await import(resolve(ROOT, 'scripts/vendor-deps.js'));
14
+ await import(resolve(ROOT, 'scripts/build-icons.js'));
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test runner — finds and executes all .test.js files.
5
+ * @module scripts/test
6
+ */
7
+
8
+ import { execSync } from 'node:child_process';
9
+ import { dirname, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { readdirSync, statSync } from 'node:fs';
12
+
13
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
14
+
15
+ /** @param {string} dir @returns {string[]} */
16
+ function findTests(dir) {
17
+ const results = [];
18
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
19
+ const full = resolve(dir, entry.name);
20
+ if (entry.name === 'node_modules' || entry.name === 'public') continue;
21
+ if (entry.isDirectory()) results.push(...findTests(full));
22
+ else if (entry.name.endsWith('.test.js')) results.push(full);
23
+ }
24
+ return results;
25
+ }
26
+
27
+ const files = findTests(ROOT);
28
+ if (files.length === 0) {
29
+ console.log('No test files found.');
30
+ process.exit(0);
31
+ }
32
+
33
+ try {
34
+ execSync(`node --test ${files.join(' ')}`, { cwd: ROOT, stdio: 'inherit' });
35
+ } catch {
36
+ process.exit(1);
37
+ }
@@ -10,7 +10,7 @@ import { resolve, dirname } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
 
12
12
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
13
- const VENDOR_DIR = resolve(ROOT, 'public/vendor');
13
+ const VENDOR_DIR = resolve(ROOT, 'src/public/vendor');
14
14
 
15
15
  /** @type {{ name: string, src: string }[]} */
16
16
  const DEPS = [{ name: 'hybrids', src: 'node_modules/hybrids/src' }];
@@ -21,5 +21,5 @@ for (const dep of DEPS) {
21
21
  const src = resolve(ROOT, dep.src);
22
22
  const dest = resolve(VENDOR_DIR, dep.name);
23
23
  cpSync(src, dest, { recursive: true });
24
- console.log(`✓ Vendored: ${dep.name} → public/vendor/${dep.name}/`);
24
+ console.log(`✓ Vendored: ${dep.name} → src/public/vendor/${dep.name}/`);
25
25
  }
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{name}}</title>
7
+ <link rel="stylesheet" href="/styles/reset.css">
8
+ <link rel="stylesheet" href="/styles/tokens.css">
9
+ <link rel="stylesheet" href="/styles/shared.css">
10
+ <link rel="stylesheet" href="/styles/buttons.css">
11
+ <link rel="stylesheet" href="/styles/forms.css">
12
+ <link rel="stylesheet" href="/styles/components.css">
13
+
14
+ <script type="importmap">
15
+ {
16
+ "imports": {
17
+ "hybrids": "/public/vendor/hybrids/index.js"
18
+ }
19
+ }
20
+ </script>
21
+ </head>
22
+ <body>
23
+ <app-router></app-router>
24
+ <script type="module" src="/router/index.js"></script>
25
+ </body>
26
+ </html>
@@ -1,26 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{{name}}</title>
7
- <link rel="stylesheet" href="/src/styles/reset.css">
8
- <link rel="stylesheet" href="/src/styles/tokens.css">
9
- <link rel="stylesheet" href="/src/styles/shared.css">
10
- <link rel="stylesheet" href="/src/styles/buttons.css">
11
- <link rel="stylesheet" href="/src/styles/forms.css">
12
- <link rel="stylesheet" href="/src/styles/components.css">
13
-
14
- <script type="importmap">
15
- {
16
- "imports": {
17
- "hybrids": "/vendor/hybrids/index.js"
18
- }
19
- }
20
- </script>
21
- </head>
22
- <body>
23
- <app-router></app-router>
24
- <script type="module" src="/src/router/index.js"></script>
25
- </body>
26
- </html>