@techninja/clearstack 0.3.3 → 0.3.6
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/docs/COMPONENT_PATTERNS.md +1 -1
- package/docs/CONVENTIONS.md +17 -0
- package/docs/FRONTEND_IMPLEMENTATION_RULES.md +31 -23
- package/docs/QUICKSTART.md +22 -31
- package/docs/SERVER_AND_DEPS.md +14 -14
- package/lib/check.js +3 -1
- package/lib/package-gen.js +1 -0
- package/package.json +3 -2
- package/templates/fullstack/src/server.js +1 -1
- package/templates/shared/.configs/eslint.config.js +7 -3
- package/templates/shared/.configs/jsconfig.json +1 -1
- package/templates/shared/.env +1 -1
- package/templates/shared/docs/clearstack/COMPONENT_PATTERNS.md +1 -1
- package/templates/shared/docs/clearstack/CONVENTIONS.md +17 -0
- package/templates/shared/docs/clearstack/FRONTEND_IMPLEMENTATION_RULES.md +31 -23
- package/templates/shared/docs/clearstack/QUICKSTART.md +22 -31
- package/templates/shared/docs/clearstack/SERVER_AND_DEPS.md +14 -14
- package/templates/shared/gitignore +2 -2
- package/templates/shared/scripts/build-icons.js +3 -3
- package/templates/shared/scripts/test.js +31 -11
- package/templates/shared/scripts/vendor-deps.js +3 -3
- package/templates/shared/src/components/atoms/app-icon/app-icon.js +4 -1
- package/templates/shared/src/{public/index.html → index.html} +1 -1
- package/templates/static/src/server.js +1 -1
|
@@ -292,7 +292,7 @@ handler resolves `host` to the page component as expected.
|
|
|
292
292
|
Because components use light DOM, styles flow naturally:
|
|
293
293
|
|
|
294
294
|
```
|
|
295
|
-
|
|
295
|
+
src/index.html
|
|
296
296
|
├── <link> src/styles/reset.css ← base reset
|
|
297
297
|
├── <link> src/styles/tokens.css ← :root custom properties
|
|
298
298
|
├── <link> src/styles/shared.css ← error states, icons, utilities
|
package/docs/CONVENTIONS.md
CHANGED
|
@@ -269,6 +269,23 @@ multiple keys.
|
|
|
269
269
|
|
|
270
270
|
---
|
|
271
271
|
|
|
272
|
+
## Spec vs Test: Two Different Questions
|
|
273
|
+
|
|
274
|
+
`spec` and `test` answer different questions and run at different times.
|
|
275
|
+
|
|
276
|
+
| | `npm run spec` | `npm test` |
|
|
277
|
+
| ---------- | ------------------------------------------ | -------------------------------- |
|
|
278
|
+
| Asks | Is my code clean? | Does my code work? |
|
|
279
|
+
| Checks | Line counts, lint, format, types, markdown | Unit, integration, browser tests |
|
|
280
|
+
| Speed | Fast (~10s) | Slower (varies) |
|
|
281
|
+
| When | Every save, every change | Before commit, in CI |
|
|
282
|
+
| Auto-fixes | Yes (lint, format, markdown) | No |
|
|
283
|
+
|
|
284
|
+
Spec is the inner dev loop — run it constantly. Tests are the commit gate —
|
|
285
|
+
run them before pushing. CI runs both, in parallel.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
272
289
|
## Session Retrospective
|
|
273
290
|
|
|
274
291
|
At the end of each implementation session, ask:
|
|
@@ -117,12 +117,12 @@ import {
|
|
|
117
117
|
|
|
118
118
|
```
|
|
119
119
|
project-root/
|
|
120
|
-
├──
|
|
120
|
+
├── src/ # The app — served directly, no build
|
|
121
121
|
│ ├── index.html # App shell with import map
|
|
122
|
-
│
|
|
123
|
-
│
|
|
124
|
-
│
|
|
125
|
-
|
|
122
|
+
│ ├── vendor/ # Vendored ES modules (generated, gitignored)
|
|
123
|
+
│ │ └── hybrids/ # Copied from node_modules at install
|
|
124
|
+
│ ├── icons.json # Icon sprite (generated, gitignored)
|
|
125
|
+
│ │
|
|
126
126
|
│ ├── components/ # UI components (atomic design)
|
|
127
127
|
│ │ ├── atoms/ # Smallest UI primitives
|
|
128
128
|
│ │ │ └── app-button/
|
|
@@ -130,39 +130,47 @@ project-root/
|
|
|
130
130
|
│ │ │ ├── app-button.css # Scoped styles
|
|
131
131
|
│ │ │ └── index.js # Re-export
|
|
132
132
|
│ │ ├── molecules/ # Compositions of atoms
|
|
133
|
-
│ │ │ └── search-bar/
|
|
134
133
|
│ │ ├── organisms/ # Complex UI sections
|
|
135
|
-
│ │ │ └── app-header/
|
|
136
134
|
│ │ └── templates/ # Page-level layout shells
|
|
137
|
-
│ │ └── page-layout/
|
|
138
135
|
│ │
|
|
139
136
|
│ ├── pages/ # Route-bound view components
|
|
140
|
-
│ │
|
|
141
|
-
│ │
|
|
142
|
-
│ │ │ └── index.js
|
|
143
|
-
│ │ └── about/
|
|
137
|
+
│ │ └── home/
|
|
138
|
+
│ │ └── home-view.js
|
|
144
139
|
│ │
|
|
145
140
|
│ ├── store/ # Hybrids store model definitions
|
|
146
141
|
│ │ ├── AppState.js # Singleton: global app state
|
|
147
|
-
│ │ └──
|
|
142
|
+
│ │ └── UserPrefs.js # Singleton: user preferences
|
|
148
143
|
│ │
|
|
149
144
|
│ ├── router/ # Router shell component
|
|
150
145
|
│ │ └── index.js
|
|
151
146
|
│ │
|
|
152
147
|
│ ├── styles/ # Shared CSS
|
|
153
148
|
│ │ ├── tokens.css # Design tokens (colors, spacing)
|
|
154
|
-
│ │
|
|
149
|
+
│ │ ├── reset.css # Minimal CSS reset
|
|
150
|
+
│ │ └── components.css # Aggregates component CSS imports
|
|
151
|
+
│ │
|
|
152
|
+
│ ├── utils/ # Pure helper functions
|
|
153
|
+
│ │ └── formatDate.js
|
|
155
154
|
│ │
|
|
156
|
-
│
|
|
157
|
-
│
|
|
155
|
+
│ ├── api/ # Server routes (fullstack only)
|
|
156
|
+
│ │ ├── entities.js # Generic CRUD router
|
|
157
|
+
│ │ └── schemas.js # JSON Schema registry
|
|
158
|
+
│ │
|
|
159
|
+
│ └── server.js # Express entry point
|
|
160
|
+
│
|
|
161
|
+
├── scripts/ # Install/setup scripts
|
|
162
|
+
│ ├── vendor-deps.js # Copies deps to src/vendor/
|
|
163
|
+
│ └── build-icons.js # Extracts Lucide SVGs to src/icons.json
|
|
158
164
|
│
|
|
159
|
-
├──
|
|
160
|
-
|
|
165
|
+
├── .configs/ # Linter/formatter/type configs
|
|
166
|
+
├── docs/
|
|
167
|
+
│ ├── clearstack/ # Spec docs (synced on update)
|
|
168
|
+
│ └── app-spec/ # Project-specific specs (yours)
|
|
161
169
|
│
|
|
162
|
-
├──
|
|
163
|
-
├──
|
|
164
|
-
├──
|
|
165
|
-
└──
|
|
170
|
+
├── data/ # JSON DB seed (fullstack only)
|
|
171
|
+
├── .env # Defaults (committed)
|
|
172
|
+
├── .env.local # Overrides (gitignored)
|
|
173
|
+
└── package.json
|
|
166
174
|
```
|
|
167
175
|
|
|
168
176
|
### Key Conventions
|
|
@@ -170,7 +178,7 @@ project-root/
|
|
|
170
178
|
- **One component per directory.** Each gets its own folder with `.js`, `.css`,
|
|
171
179
|
and `index.js` (re-export).
|
|
172
180
|
- **`src/` is served as-is** — the browser loads these files directly.
|
|
173
|
-
- **`
|
|
181
|
+
- **`src/vendor/`** is gitignored and regenerated on `npm install` via the
|
|
174
182
|
`postinstall` script.
|
|
175
183
|
- **No barrel files** beyond the per-component `index.js`. Import from the
|
|
176
184
|
component directory, not from a giant `components/index.js`.
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -42,33 +42,30 @@ If a `package.json` already exists, Clearstack merges into it — your existing
|
|
|
42
42
|
```
|
|
43
43
|
your-project/
|
|
44
44
|
├── .configs/ # ⟳ Managed — synced on update
|
|
45
|
-
│ ├── eslint.config.js
|
|
46
|
-
│ ├── .prettierrc
|
|
47
|
-
│ ├── jsconfig.json
|
|
48
|
-
│ └── web-test-runner.config.js
|
|
49
45
|
├── .github/ # CI workflow, PR + issue templates
|
|
50
46
|
├── docs/
|
|
51
47
|
│ ├── clearstack/ # ⟳ Managed — spec docs, synced on update
|
|
52
48
|
│ └── app-spec/ # ✏️ Yours — project-specific specs
|
|
53
|
-
├──
|
|
54
|
-
├──
|
|
55
|
-
├──
|
|
56
|
-
│ ├──
|
|
57
|
-
│ ├──
|
|
58
|
-
│ ├──
|
|
59
|
-
│ ├──
|
|
60
|
-
│ ├──
|
|
61
|
-
│ ├──
|
|
62
|
-
│ ├──
|
|
63
|
-
│
|
|
64
|
-
├──
|
|
65
|
-
|
|
66
|
-
|
|
49
|
+
├── scripts/ # ✏️ Yours — setup, test, vendor-deps, build-icons
|
|
50
|
+
├── src/ # ✏️ Yours — the app, served directly
|
|
51
|
+
│ ├── index.html # App shell with import map
|
|
52
|
+
│ ├── vendor/ # Vendored ES modules (generated, gitignored)
|
|
53
|
+
│ ├── icons.json # Icon sprite (generated, gitignored)
|
|
54
|
+
│ ├── components/ # atoms/, molecules/, organisms/
|
|
55
|
+
│ ├── pages/ # Route-level views
|
|
56
|
+
│ ├── store/ # Hybrids store models
|
|
57
|
+
│ ├── styles/ # Global CSS
|
|
58
|
+
│ ├── router/ # Client-side routing
|
|
59
|
+
│ ├── utils/ # Shared helpers
|
|
60
|
+
│ ├── api/ # Server routes (fullstack only)
|
|
61
|
+
│ └── server.js # Express entry point
|
|
62
|
+
├── data/ # JSON DB seed (fullstack only)
|
|
63
|
+
├── .env # Defaults (committed)
|
|
64
|
+
├── .env.local # Overrides (gitignored)
|
|
65
|
+
└── package.json
|
|
67
66
|
```
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
**✏️ Yours** files are scaffolded once and never touched by updates. They're your code.
|
|
68
|
+
`src/` is the entire app. `node src/server.js` serves it. For static deploy, point your CDN at `src/`. No build step.
|
|
72
69
|
|
|
73
70
|
## 4. Install and Run
|
|
74
71
|
|
|
@@ -76,18 +73,12 @@ your-project/
|
|
|
76
73
|
npm install
|
|
77
74
|
```
|
|
78
75
|
|
|
79
|
-
`postinstall` runs `vendor-deps.js` (copies hybrids to `
|
|
76
|
+
`postinstall` runs `vendor-deps.js` (copies hybrids to `src/vendor/`) and `build-icons.js` (extracts Lucide SVGs to `src/icons.json`).
|
|
80
77
|
|
|
81
78
|
### Fullstack
|
|
82
79
|
|
|
83
80
|
```bash
|
|
84
|
-
npm run dev
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Static
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
npx serve public # or any static file server
|
|
81
|
+
npm run dev
|
|
91
82
|
```
|
|
92
83
|
|
|
93
84
|
## 5. Development Rules
|
|
@@ -152,7 +143,7 @@ SPEC_CODE_MAX_LINES=150
|
|
|
152
143
|
SPEC_DOCS_MAX_LINES=500
|
|
153
144
|
SPEC_CODE_EXTENSIONS=.js,.css
|
|
154
145
|
SPEC_DOCS_EXTENSIONS=.md
|
|
155
|
-
SPEC_IGNORE_DIRS=node_modules,
|
|
146
|
+
SPEC_IGNORE_DIRS=node_modules,src/vendor,.git,.configs
|
|
156
147
|
```
|
|
157
148
|
|
|
158
149
|
## 9. CI Pipeline
|
|
@@ -170,7 +161,7 @@ spec:code → spec:docs → lint → format → typecheck → test
|
|
|
170
161
|
| Install Clearstack | `npm install -D @techninja/clearstack` |
|
|
171
162
|
| Scaffold a project | `npx clearstack init` |
|
|
172
163
|
| Install dependencies | `npm install` |
|
|
173
|
-
| Start dev server | `npm run dev`
|
|
164
|
+
| Start dev server | `npm run dev` |
|
|
174
165
|
| Lint + format | `npm run lint:fix && npm run format` |
|
|
175
166
|
| Type check | `npm run typecheck` |
|
|
176
167
|
| Run tests | `npm test` |
|
package/docs/SERVER_AND_DEPS.md
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
### Responsibilities
|
|
16
16
|
|
|
17
|
-
| Route | Serves
|
|
18
|
-
| ------------- |
|
|
19
|
-
| `/` | `
|
|
20
|
-
| `/vendor/*` | `
|
|
21
|
-
| `/src/*` | `src/` (application source, as-is)
|
|
22
|
-
| `/api/*` | REST endpoints (see BACKEND_API_SPEC)
|
|
23
|
-
| `/api/events` | SSE stream for realtime sync
|
|
17
|
+
| Route | Serves |
|
|
18
|
+
| ------------- | ------------------------------------- |
|
|
19
|
+
| `/` | `src/index.html` (app shell) |
|
|
20
|
+
| `/vendor/*` | `src/vendor/` (vendored ES modules) |
|
|
21
|
+
| `/src/*` | `src/` (application source, as-is) |
|
|
22
|
+
| `/api/*` | REST endpoints (see BACKEND_API_SPEC) |
|
|
23
|
+
| `/api/events` | SSE stream for realtime sync |
|
|
24
24
|
|
|
25
25
|
### Key Rules
|
|
26
26
|
|
|
@@ -38,7 +38,7 @@ const app = express();
|
|
|
38
38
|
const PORT = process.env.PORT || 3000;
|
|
39
39
|
|
|
40
40
|
// Static: vendored deps and app shell
|
|
41
|
-
app.use(express.static('
|
|
41
|
+
app.use(express.static('src'));
|
|
42
42
|
|
|
43
43
|
// Static: application source (ES modules served directly)
|
|
44
44
|
app.use('/src', express.static('src'));
|
|
@@ -57,7 +57,7 @@ app.listen(PORT, () => console.log(`http://localhost:${PORT}`));
|
|
|
57
57
|
## Vendor Dependency Loading
|
|
58
58
|
|
|
59
59
|
Third-party ES module packages are copied from `node_modules/` into
|
|
60
|
-
`
|
|
60
|
+
`src/vendor/` at install time. This makes them servable as static files.
|
|
61
61
|
|
|
62
62
|
### scripts/vendor-deps.js
|
|
63
63
|
|
|
@@ -65,7 +65,7 @@ Third-party ES module packages are copied from `node_modules/` into
|
|
|
65
65
|
import { cpSync, mkdirSync } from 'node:fs';
|
|
66
66
|
import { resolve } from 'node:path';
|
|
67
67
|
|
|
68
|
-
const VENDOR_DIR = resolve('
|
|
68
|
+
const VENDOR_DIR = resolve('src/vendor');
|
|
69
69
|
const DEPS = [{ name: 'hybrids', src: 'node_modules/hybrids/src' }];
|
|
70
70
|
|
|
71
71
|
mkdirSync(VENDOR_DIR, { recursive: true });
|
|
@@ -89,7 +89,7 @@ In `package.json`:
|
|
|
89
89
|
}
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
Running `npm install` automatically vendors dependencies. The `
|
|
92
|
+
Running `npm install` automatically vendors dependencies. The `src/vendor/`
|
|
93
93
|
directory should be in `.gitignore`.
|
|
94
94
|
|
|
95
95
|
### Adding a New Dependency
|
|
@@ -104,9 +104,9 @@ directory should be in `.gitignore`.
|
|
|
104
104
|
## Import Map
|
|
105
105
|
|
|
106
106
|
The browser resolves bare specifiers like `'hybrids'` via an import map
|
|
107
|
-
declared in `
|
|
107
|
+
declared in `src/index.html`.
|
|
108
108
|
|
|
109
|
-
###
|
|
109
|
+
### src/index.html
|
|
110
110
|
|
|
111
111
|
```html
|
|
112
112
|
<!DOCTYPE html>
|
|
@@ -137,7 +137,7 @@ declared in `public/index.html`.
|
|
|
137
137
|
|
|
138
138
|
1. Browser encounters `import { html } from 'hybrids'` in any ES module.
|
|
139
139
|
2. Import map resolves `'hybrids'` → `/vendor/hybrids/index.js`.
|
|
140
|
-
3. Server serves the file from `
|
|
140
|
+
3. Server serves the file from `src/vendor/hybrids/index.js`.
|
|
141
141
|
4. Hybrids' internal imports use relative paths — they resolve naturally.
|
|
142
142
|
|
|
143
143
|
### Rules
|
package/lib/check.js
CHANGED
|
@@ -32,7 +32,7 @@ export function loadConfig(projectDir) {
|
|
|
32
32
|
docsMax: parseInt(env.SPEC_DOCS_MAX_LINES) || 500,
|
|
33
33
|
codeExt: (env.SPEC_CODE_EXTENSIONS || '.js,.css').split(','),
|
|
34
34
|
docsExt: (env.SPEC_DOCS_EXTENSIONS || '.md').split(','),
|
|
35
|
-
ignore: (env.SPEC_IGNORE_DIRS || 'node_modules,src/
|
|
35
|
+
ignore: (env.SPEC_IGNORE_DIRS || 'node_modules,src/vendor,.git,.configs').split(','),
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -43,6 +43,7 @@ export const CMDS = {
|
|
|
43
43
|
prettier: 'npx prettier --config .configs/.prettierrc --write src scripts',
|
|
44
44
|
mdlint: 'npx markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"',
|
|
45
45
|
types: 'npx tsc --project .configs/jsconfig.json',
|
|
46
|
+
audit: 'npm audit --omit=dev',
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
/**
|
|
@@ -77,6 +78,7 @@ export async function check(projectDir, scope) {
|
|
|
77
78
|
runCmd('Prettier', CMDS.prettier, projectDir, `${jsFiles} files`),
|
|
78
79
|
runCmd('Markdown', CMDS.mdlint, projectDir, `${mdFiles} files`),
|
|
79
80
|
runCmd('JSDoc types', CMDS.types, projectDir, `${jsFiles} files`),
|
|
81
|
+
runCmd('Security audit', CMDS.audit, projectDir),
|
|
80
82
|
];
|
|
81
83
|
|
|
82
84
|
const passed = results.filter(Boolean).length;
|
package/lib/package-gen.js
CHANGED
|
@@ -40,6 +40,7 @@ export async function writePackageJson(dest, vars, existing) {
|
|
|
40
40
|
eslint: '^10.1.0',
|
|
41
41
|
'eslint-config-prettier': '^10.1.8',
|
|
42
42
|
'eslint-plugin-jsdoc': '^62.8.1',
|
|
43
|
+
'eslint-plugin-unused-imports': '^4.0.0',
|
|
43
44
|
'markdownlint-cli2': '^0.22.0',
|
|
44
45
|
playwright: '^1.50.0',
|
|
45
46
|
prettier: '^3.8.1',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techninja/clearstack",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"start": "node src/server.js",
|
|
21
21
|
"dev": "node --watch --env-file=.env --env-file=.env.local src/server.js",
|
|
22
22
|
"setup": "node scripts/vendor-deps.js && node scripts/build-icons.js",
|
|
23
|
-
"test": "node
|
|
23
|
+
"test": "node scripts/test.js",
|
|
24
24
|
"spec": "node --env-file=.env scripts/spec.js",
|
|
25
25
|
"lint": "eslint --config .configs/eslint.config.js . --fix",
|
|
26
26
|
"format": "prettier --config .configs/.prettierrc --write src scripts tests templates/**/*.md templates/**/*.html templates/**/*.css templates/**/*.json",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"eslint": "^10.1.0",
|
|
52
52
|
"eslint-config-prettier": "^10.1.8",
|
|
53
53
|
"eslint-plugin-jsdoc": "^62.8.1",
|
|
54
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
54
55
|
"express": "^5.2.1",
|
|
55
56
|
"hybrids": "^9.1.22",
|
|
56
57
|
"lucide-static": "^1.7.0",
|
|
@@ -19,7 +19,7 @@ app.use(express.static('src'));
|
|
|
19
19
|
// SPA fallback
|
|
20
20
|
app.use((req, res, next) => {
|
|
21
21
|
if (req.method === 'GET' && !req.path.includes('.') && !req.path.startsWith('/api')) {
|
|
22
|
-
return res.sendFile('index.html', { root: 'src
|
|
22
|
+
return res.sendFile('index.html', { root: 'src' });
|
|
23
23
|
}
|
|
24
24
|
next();
|
|
25
25
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import prettier from 'eslint-config-prettier';
|
|
2
2
|
import jsdoc from 'eslint-plugin-jsdoc';
|
|
3
|
+
import unusedImports from 'eslint-plugin-unused-imports';
|
|
3
4
|
|
|
4
5
|
export default [
|
|
5
6
|
{
|
|
6
7
|
files: ['**/*.js'],
|
|
7
|
-
plugins: { jsdoc },
|
|
8
|
+
plugins: { jsdoc, 'unused-imports': unusedImports },
|
|
8
9
|
languageOptions: {
|
|
9
10
|
ecmaVersion: 2024,
|
|
10
11
|
sourceType: 'module',
|
|
@@ -34,7 +35,9 @@ export default [
|
|
|
34
35
|
'no-var': 'error',
|
|
35
36
|
'prefer-const': 'error',
|
|
36
37
|
eqeqeq: ['error', 'always'],
|
|
37
|
-
'no-unused-vars':
|
|
38
|
+
'no-unused-vars': 'off',
|
|
39
|
+
'unused-imports/no-unused-imports': 'error',
|
|
40
|
+
'unused-imports/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
38
41
|
'no-console': 'off',
|
|
39
42
|
|
|
40
43
|
// JSDoc enforcement
|
|
@@ -55,10 +58,11 @@ export default [
|
|
|
55
58
|
rules: {
|
|
56
59
|
'jsdoc/require-jsdoc': 'off',
|
|
57
60
|
'no-unused-vars': 'off',
|
|
61
|
+
'unused-imports/no-unused-vars': 'off',
|
|
58
62
|
},
|
|
59
63
|
},
|
|
60
64
|
{
|
|
61
|
-
ignores: ['node_modules/', 'src/
|
|
65
|
+
ignores: ['node_modules/', 'src/vendor/'],
|
|
62
66
|
},
|
|
63
67
|
prettier,
|
|
64
68
|
];
|
package/templates/shared/.env
CHANGED
|
@@ -292,7 +292,7 @@ handler resolves `host` to the page component as expected.
|
|
|
292
292
|
Because components use light DOM, styles flow naturally:
|
|
293
293
|
|
|
294
294
|
```
|
|
295
|
-
|
|
295
|
+
src/index.html
|
|
296
296
|
├── <link> src/styles/reset.css ← base reset
|
|
297
297
|
├── <link> src/styles/tokens.css ← :root custom properties
|
|
298
298
|
├── <link> src/styles/shared.css ← error states, icons, utilities
|
|
@@ -269,6 +269,23 @@ multiple keys.
|
|
|
269
269
|
|
|
270
270
|
---
|
|
271
271
|
|
|
272
|
+
## Spec vs Test: Two Different Questions
|
|
273
|
+
|
|
274
|
+
`spec` and `test` answer different questions and run at different times.
|
|
275
|
+
|
|
276
|
+
| | `npm run spec` | `npm test` |
|
|
277
|
+
| ---------- | ------------------------------------------ | -------------------------------- |
|
|
278
|
+
| Asks | Is my code clean? | Does my code work? |
|
|
279
|
+
| Checks | Line counts, lint, format, types, markdown | Unit, integration, browser tests |
|
|
280
|
+
| Speed | Fast (~10s) | Slower (varies) |
|
|
281
|
+
| When | Every save, every change | Before commit, in CI |
|
|
282
|
+
| Auto-fixes | Yes (lint, format, markdown) | No |
|
|
283
|
+
|
|
284
|
+
Spec is the inner dev loop — run it constantly. Tests are the commit gate —
|
|
285
|
+
run them before pushing. CI runs both, in parallel.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
272
289
|
## Session Retrospective
|
|
273
290
|
|
|
274
291
|
At the end of each implementation session, ask:
|
|
@@ -117,12 +117,12 @@ import {
|
|
|
117
117
|
|
|
118
118
|
```
|
|
119
119
|
project-root/
|
|
120
|
-
├──
|
|
120
|
+
├── src/ # The app — served directly, no build
|
|
121
121
|
│ ├── index.html # App shell with import map
|
|
122
|
-
│
|
|
123
|
-
│
|
|
124
|
-
│
|
|
125
|
-
|
|
122
|
+
│ ├── vendor/ # Vendored ES modules (generated, gitignored)
|
|
123
|
+
│ │ └── hybrids/ # Copied from node_modules at install
|
|
124
|
+
│ ├── icons.json # Icon sprite (generated, gitignored)
|
|
125
|
+
│ │
|
|
126
126
|
│ ├── components/ # UI components (atomic design)
|
|
127
127
|
│ │ ├── atoms/ # Smallest UI primitives
|
|
128
128
|
│ │ │ └── app-button/
|
|
@@ -130,39 +130,47 @@ project-root/
|
|
|
130
130
|
│ │ │ ├── app-button.css # Scoped styles
|
|
131
131
|
│ │ │ └── index.js # Re-export
|
|
132
132
|
│ │ ├── molecules/ # Compositions of atoms
|
|
133
|
-
│ │ │ └── search-bar/
|
|
134
133
|
│ │ ├── organisms/ # Complex UI sections
|
|
135
|
-
│ │ │ └── app-header/
|
|
136
134
|
│ │ └── templates/ # Page-level layout shells
|
|
137
|
-
│ │ └── page-layout/
|
|
138
135
|
│ │
|
|
139
136
|
│ ├── pages/ # Route-bound view components
|
|
140
|
-
│ │
|
|
141
|
-
│ │
|
|
142
|
-
│ │ │ └── index.js
|
|
143
|
-
│ │ └── about/
|
|
137
|
+
│ │ └── home/
|
|
138
|
+
│ │ └── home-view.js
|
|
144
139
|
│ │
|
|
145
140
|
│ ├── store/ # Hybrids store model definitions
|
|
146
141
|
│ │ ├── AppState.js # Singleton: global app state
|
|
147
|
-
│ │ └──
|
|
142
|
+
│ │ └── UserPrefs.js # Singleton: user preferences
|
|
148
143
|
│ │
|
|
149
144
|
│ ├── router/ # Router shell component
|
|
150
145
|
│ │ └── index.js
|
|
151
146
|
│ │
|
|
152
147
|
│ ├── styles/ # Shared CSS
|
|
153
148
|
│ │ ├── tokens.css # Design tokens (colors, spacing)
|
|
154
|
-
│ │
|
|
149
|
+
│ │ ├── reset.css # Minimal CSS reset
|
|
150
|
+
│ │ └── components.css # Aggregates component CSS imports
|
|
151
|
+
│ │
|
|
152
|
+
│ ├── utils/ # Pure helper functions
|
|
153
|
+
│ │ └── formatDate.js
|
|
155
154
|
│ │
|
|
156
|
-
│
|
|
157
|
-
│
|
|
155
|
+
│ ├── api/ # Server routes (fullstack only)
|
|
156
|
+
│ │ ├── entities.js # Generic CRUD router
|
|
157
|
+
│ │ └── schemas.js # JSON Schema registry
|
|
158
|
+
│ │
|
|
159
|
+
│ └── server.js # Express entry point
|
|
160
|
+
│
|
|
161
|
+
├── scripts/ # Install/setup scripts
|
|
162
|
+
│ ├── vendor-deps.js # Copies deps to src/vendor/
|
|
163
|
+
│ └── build-icons.js # Extracts Lucide SVGs to src/icons.json
|
|
158
164
|
│
|
|
159
|
-
├──
|
|
160
|
-
|
|
165
|
+
├── .configs/ # Linter/formatter/type configs
|
|
166
|
+
├── docs/
|
|
167
|
+
│ ├── clearstack/ # Spec docs (synced on update)
|
|
168
|
+
│ └── app-spec/ # Project-specific specs (yours)
|
|
161
169
|
│
|
|
162
|
-
├──
|
|
163
|
-
├──
|
|
164
|
-
├──
|
|
165
|
-
└──
|
|
170
|
+
├── data/ # JSON DB seed (fullstack only)
|
|
171
|
+
├── .env # Defaults (committed)
|
|
172
|
+
├── .env.local # Overrides (gitignored)
|
|
173
|
+
└── package.json
|
|
166
174
|
```
|
|
167
175
|
|
|
168
176
|
### Key Conventions
|
|
@@ -170,7 +178,7 @@ project-root/
|
|
|
170
178
|
- **One component per directory.** Each gets its own folder with `.js`, `.css`,
|
|
171
179
|
and `index.js` (re-export).
|
|
172
180
|
- **`src/` is served as-is** — the browser loads these files directly.
|
|
173
|
-
- **`
|
|
181
|
+
- **`src/vendor/`** is gitignored and regenerated on `npm install` via the
|
|
174
182
|
`postinstall` script.
|
|
175
183
|
- **No barrel files** beyond the per-component `index.js`. Import from the
|
|
176
184
|
component directory, not from a giant `components/index.js`.
|
|
@@ -42,33 +42,30 @@ If a `package.json` already exists, Clearstack merges into it — your existing
|
|
|
42
42
|
```
|
|
43
43
|
your-project/
|
|
44
44
|
├── .configs/ # ⟳ Managed — synced on update
|
|
45
|
-
│ ├── eslint.config.js
|
|
46
|
-
│ ├── .prettierrc
|
|
47
|
-
│ ├── jsconfig.json
|
|
48
|
-
│ └── web-test-runner.config.js
|
|
49
45
|
├── .github/ # CI workflow, PR + issue templates
|
|
50
46
|
├── docs/
|
|
51
47
|
│ ├── clearstack/ # ⟳ Managed — spec docs, synced on update
|
|
52
48
|
│ └── app-spec/ # ✏️ Yours — project-specific specs
|
|
53
|
-
├──
|
|
54
|
-
├──
|
|
55
|
-
├──
|
|
56
|
-
│ ├──
|
|
57
|
-
│ ├──
|
|
58
|
-
│ ├──
|
|
59
|
-
│ ├──
|
|
60
|
-
│ ├──
|
|
61
|
-
│ ├──
|
|
62
|
-
│ ├──
|
|
63
|
-
│
|
|
64
|
-
├──
|
|
65
|
-
|
|
66
|
-
|
|
49
|
+
├── scripts/ # ✏️ Yours — setup, test, vendor-deps, build-icons
|
|
50
|
+
├── src/ # ✏️ Yours — the app, served directly
|
|
51
|
+
│ ├── index.html # App shell with import map
|
|
52
|
+
│ ├── vendor/ # Vendored ES modules (generated, gitignored)
|
|
53
|
+
│ ├── icons.json # Icon sprite (generated, gitignored)
|
|
54
|
+
│ ├── components/ # atoms/, molecules/, organisms/
|
|
55
|
+
│ ├── pages/ # Route-level views
|
|
56
|
+
│ ├── store/ # Hybrids store models
|
|
57
|
+
│ ├── styles/ # Global CSS
|
|
58
|
+
│ ├── router/ # Client-side routing
|
|
59
|
+
│ ├── utils/ # Shared helpers
|
|
60
|
+
│ ├── api/ # Server routes (fullstack only)
|
|
61
|
+
│ └── server.js # Express entry point
|
|
62
|
+
├── data/ # JSON DB seed (fullstack only)
|
|
63
|
+
├── .env # Defaults (committed)
|
|
64
|
+
├── .env.local # Overrides (gitignored)
|
|
65
|
+
└── package.json
|
|
67
66
|
```
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
**✏️ Yours** files are scaffolded once and never touched by updates. They're your code.
|
|
68
|
+
`src/` is the entire app. `node src/server.js` serves it. For static deploy, point your CDN at `src/`. No build step.
|
|
72
69
|
|
|
73
70
|
## 4. Install and Run
|
|
74
71
|
|
|
@@ -76,18 +73,12 @@ your-project/
|
|
|
76
73
|
npm install
|
|
77
74
|
```
|
|
78
75
|
|
|
79
|
-
`postinstall` runs `vendor-deps.js` (copies hybrids to `
|
|
76
|
+
`postinstall` runs `vendor-deps.js` (copies hybrids to `src/vendor/`) and `build-icons.js` (extracts Lucide SVGs to `src/icons.json`).
|
|
80
77
|
|
|
81
78
|
### Fullstack
|
|
82
79
|
|
|
83
80
|
```bash
|
|
84
|
-
npm run dev
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Static
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
npx serve public # or any static file server
|
|
81
|
+
npm run dev
|
|
91
82
|
```
|
|
92
83
|
|
|
93
84
|
## 5. Development Rules
|
|
@@ -152,7 +143,7 @@ SPEC_CODE_MAX_LINES=150
|
|
|
152
143
|
SPEC_DOCS_MAX_LINES=500
|
|
153
144
|
SPEC_CODE_EXTENSIONS=.js,.css
|
|
154
145
|
SPEC_DOCS_EXTENSIONS=.md
|
|
155
|
-
SPEC_IGNORE_DIRS=node_modules,
|
|
146
|
+
SPEC_IGNORE_DIRS=node_modules,src/vendor,.git,.configs
|
|
156
147
|
```
|
|
157
148
|
|
|
158
149
|
## 9. CI Pipeline
|
|
@@ -170,7 +161,7 @@ spec:code → spec:docs → lint → format → typecheck → test
|
|
|
170
161
|
| Install Clearstack | `npm install -D @techninja/clearstack` |
|
|
171
162
|
| Scaffold a project | `npx clearstack init` |
|
|
172
163
|
| Install dependencies | `npm install` |
|
|
173
|
-
| Start dev server | `npm run dev`
|
|
164
|
+
| Start dev server | `npm run dev` |
|
|
174
165
|
| Lint + format | `npm run lint:fix && npm run format` |
|
|
175
166
|
| Type check | `npm run typecheck` |
|
|
176
167
|
| Run tests | `npm test` |
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
### Responsibilities
|
|
16
16
|
|
|
17
|
-
| Route | Serves
|
|
18
|
-
| ------------- |
|
|
19
|
-
| `/` | `
|
|
20
|
-
| `/vendor/*` | `
|
|
21
|
-
| `/src/*` | `src/` (application source, as-is)
|
|
22
|
-
| `/api/*` | REST endpoints (see BACKEND_API_SPEC)
|
|
23
|
-
| `/api/events` | SSE stream for realtime sync
|
|
17
|
+
| Route | Serves |
|
|
18
|
+
| ------------- | ------------------------------------- |
|
|
19
|
+
| `/` | `src/index.html` (app shell) |
|
|
20
|
+
| `/vendor/*` | `src/vendor/` (vendored ES modules) |
|
|
21
|
+
| `/src/*` | `src/` (application source, as-is) |
|
|
22
|
+
| `/api/*` | REST endpoints (see BACKEND_API_SPEC) |
|
|
23
|
+
| `/api/events` | SSE stream for realtime sync |
|
|
24
24
|
|
|
25
25
|
### Key Rules
|
|
26
26
|
|
|
@@ -38,7 +38,7 @@ const app = express();
|
|
|
38
38
|
const PORT = process.env.PORT || 3000;
|
|
39
39
|
|
|
40
40
|
// Static: vendored deps and app shell
|
|
41
|
-
app.use(express.static('
|
|
41
|
+
app.use(express.static('src'));
|
|
42
42
|
|
|
43
43
|
// Static: application source (ES modules served directly)
|
|
44
44
|
app.use('/src', express.static('src'));
|
|
@@ -57,7 +57,7 @@ app.listen(PORT, () => console.log(`http://localhost:${PORT}`));
|
|
|
57
57
|
## Vendor Dependency Loading
|
|
58
58
|
|
|
59
59
|
Third-party ES module packages are copied from `node_modules/` into
|
|
60
|
-
`
|
|
60
|
+
`src/vendor/` at install time. This makes them servable as static files.
|
|
61
61
|
|
|
62
62
|
### scripts/vendor-deps.js
|
|
63
63
|
|
|
@@ -65,7 +65,7 @@ Third-party ES module packages are copied from `node_modules/` into
|
|
|
65
65
|
import { cpSync, mkdirSync } from 'node:fs';
|
|
66
66
|
import { resolve } from 'node:path';
|
|
67
67
|
|
|
68
|
-
const VENDOR_DIR = resolve('
|
|
68
|
+
const VENDOR_DIR = resolve('src/vendor');
|
|
69
69
|
const DEPS = [{ name: 'hybrids', src: 'node_modules/hybrids/src' }];
|
|
70
70
|
|
|
71
71
|
mkdirSync(VENDOR_DIR, { recursive: true });
|
|
@@ -89,7 +89,7 @@ In `package.json`:
|
|
|
89
89
|
}
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
Running `npm install` automatically vendors dependencies. The `
|
|
92
|
+
Running `npm install` automatically vendors dependencies. The `src/vendor/`
|
|
93
93
|
directory should be in `.gitignore`.
|
|
94
94
|
|
|
95
95
|
### Adding a New Dependency
|
|
@@ -104,9 +104,9 @@ directory should be in `.gitignore`.
|
|
|
104
104
|
## Import Map
|
|
105
105
|
|
|
106
106
|
The browser resolves bare specifiers like `'hybrids'` via an import map
|
|
107
|
-
declared in `
|
|
107
|
+
declared in `src/index.html`.
|
|
108
108
|
|
|
109
|
-
###
|
|
109
|
+
### src/index.html
|
|
110
110
|
|
|
111
111
|
```html
|
|
112
112
|
<!DOCTYPE html>
|
|
@@ -137,7 +137,7 @@ declared in `public/index.html`.
|
|
|
137
137
|
|
|
138
138
|
1. Browser encounters `import { html } from 'hybrids'` in any ES module.
|
|
139
139
|
2. Import map resolves `'hybrids'` → `/vendor/hybrids/index.js`.
|
|
140
|
-
3. Server serves the file from `
|
|
140
|
+
3. Server serves the file from `src/vendor/hybrids/index.js`.
|
|
141
141
|
4. Hybrids' internal imports use relative paths — they resolve naturally.
|
|
142
142
|
|
|
143
143
|
### Rules
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Extracts SVG path data from lucide-static for icons used in the app.
|
|
5
|
-
* Generates
|
|
5
|
+
* Generates src/icons.json — loaded by app-icon at runtime.
|
|
6
6
|
* Runs on `npm postinstall`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -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, 'src/
|
|
15
|
+
const OUT = resolve(ROOT, 'src/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 → src/
|
|
86
|
+
console.log(`✓ Built ${count} icons → src/icons.json`);
|
|
@@ -1,37 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Test runner —
|
|
4
|
+
* Test runner — runs node tests directly, browser tests via web-test-runner.
|
|
5
|
+
* Browser tests live in src/components/. Everything else runs in Node.
|
|
5
6
|
* @module scripts/test
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { execSync } from 'node:child_process';
|
|
9
10
|
import { dirname, resolve } from 'node:path';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { readdirSync,
|
|
12
|
+
import { readdirSync, existsSync } from 'node:fs';
|
|
12
13
|
|
|
13
14
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
14
15
|
|
|
15
16
|
/** @param {string} dir @returns {string[]} */
|
|
16
17
|
function findTests(dir) {
|
|
17
18
|
const results = [];
|
|
19
|
+
if (!existsSync(dir)) return results;
|
|
18
20
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
19
21
|
const full = resolve(dir, entry.name);
|
|
20
|
-
if (entry.name === 'node_modules' || entry.name === '
|
|
22
|
+
if (entry.name === 'node_modules' || entry.name === 'vendor') continue;
|
|
21
23
|
if (entry.isDirectory()) results.push(...findTests(full));
|
|
22
24
|
else if (entry.name.endsWith('.test.js')) results.push(full);
|
|
23
25
|
}
|
|
24
26
|
return results;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const componentDir = resolve(ROOT, 'src/components');
|
|
30
|
+
const allTests = findTests(ROOT);
|
|
31
|
+
const nodeTests = allTests.filter((f) => !f.startsWith(componentDir));
|
|
32
|
+
const browserTests = allTests.filter((f) => f.startsWith(componentDir));
|
|
33
|
+
let failed = false;
|
|
34
|
+
|
|
35
|
+
if (nodeTests.length > 0) {
|
|
36
|
+
console.log(`Running ${nodeTests.length} node test(s)...\n`);
|
|
37
|
+
try {
|
|
38
|
+
execSync(`node --test ${nodeTests.join(' ')}`, { cwd: ROOT, stdio: 'inherit' });
|
|
39
|
+
} catch {
|
|
40
|
+
failed = true;
|
|
41
|
+
}
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
if (browserTests.length > 0) {
|
|
45
|
+
console.log(`\nRunning ${browserTests.length} browser test(s)...\n`);
|
|
46
|
+
try {
|
|
47
|
+
execSync('npx web-test-runner --config .configs/web-test-runner.config.js', {
|
|
48
|
+
cwd: ROOT,
|
|
49
|
+
stdio: 'inherit',
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
failed = true;
|
|
53
|
+
}
|
|
37
54
|
}
|
|
55
|
+
|
|
56
|
+
if (allTests.length === 0) console.log('No test files found.');
|
|
57
|
+
if (failed) process.exit(1);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Copies third-party ES module sources into
|
|
4
|
+
* Copies third-party ES module sources into src/vendor/ so the browser
|
|
5
5
|
* can load them directly via import map. Runs on `npm postinstall`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -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, 'src/
|
|
13
|
+
const VENDOR_DIR = resolve(ROOT, 'src/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} → src/
|
|
24
|
+
console.log(`✓ Vendored: ${dep.name} → src/vendor/${dep.name}/`);
|
|
25
25
|
}
|
|
@@ -11,7 +11,10 @@ let iconCache = null;
|
|
|
11
11
|
|
|
12
12
|
/** @type {Promise<Record<string, string>>} */
|
|
13
13
|
const loading = fetch('/icons.json')
|
|
14
|
-
.then((r) =>
|
|
14
|
+
.then((r) => {
|
|
15
|
+
if (r.ok) return r;
|
|
16
|
+
throw new Error('icons.json not found');
|
|
17
|
+
})
|
|
15
18
|
.then((r) => r.json())
|
|
16
19
|
.then((data) => {
|
|
17
20
|
iconCache = data;
|
|
@@ -12,7 +12,7 @@ app.use(express.static('src'));
|
|
|
12
12
|
|
|
13
13
|
app.use((req, res, next) => {
|
|
14
14
|
if (req.method === 'GET' && !req.path.includes('.')) {
|
|
15
|
-
return res.sendFile('index.html', { root: 'src
|
|
15
|
+
return res.sendFile('index.html', { root: 'src' });
|
|
16
16
|
}
|
|
17
17
|
next();
|
|
18
18
|
});
|