@techninja/clearstack 0.3.15 → 0.3.17
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/README.md +8 -6
- package/bin/cli.js +2 -2
- package/docs/BUILD_LOG.md +97 -0
- package/docs/CONVENTIONS.md +3 -2
- package/docs/JSDOC_TYPING.md +24 -3
- package/docs/QUICKSTART.md +30 -24
- package/docs/TESTING.md +52 -19
- package/lib/check.js +21 -5
- package/lib/update.js +16 -8
- package/package.json +1 -1
- package/templates/shared/.configs/web-test-runner.config.js +28 -1
- package/templates/shared/docs/clearstack/BUILD_LOG.md +97 -0
- package/templates/shared/docs/clearstack/CONVENTIONS.md +3 -2
- package/templates/shared/docs/clearstack/JSDOC_TYPING.md +24 -3
- package/templates/shared/docs/clearstack/QUICKSTART.md +30 -24
- package/templates/shared/docs/clearstack/TESTING.md +52 -19
package/README.md
CHANGED
|
@@ -50,8 +50,9 @@ npm install -D @techninja/clearstack # add to your project
|
|
|
50
50
|
npx clearstack init # scaffold (interactive)
|
|
51
51
|
npx clearstack init -y # scaffold (fullstack defaults)
|
|
52
52
|
npx clearstack init --static # scaffold (static, no server)
|
|
53
|
+
npx clearstack update # sync docs (skip existing configs)
|
|
54
|
+
npx clearstack update --force # sync docs + overwrite configs
|
|
53
55
|
npm run spec # check compliance
|
|
54
|
-
npm run spec:update # sync docs + configs on upgrade
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
Two modes: **fullstack** (Express + WebSocket + JSON DB + SSE) or **static** (localStorage, no server).
|
|
@@ -65,7 +66,7 @@ See [QUICKSTART.md](./docs/QUICKSTART.md) for the full walkthrough.
|
|
|
65
66
|
- **Light DOM by default.** Shared styles just work.
|
|
66
67
|
- **JSDoc over TypeScript.** Types without a compile step — validated by `tsc --checkJs`.
|
|
67
68
|
- **Test at the boundary.** Each phase passes before the next begins.
|
|
68
|
-
- **The spec checks itself.** `npm run spec
|
|
69
|
+
- **The spec checks itself.** `npm run spec code` and `npm run spec docs`.
|
|
69
70
|
- **Lint and format.** ESLint + Prettier, semicolons, 2-space indent.
|
|
70
71
|
|
|
71
72
|
## Scripts
|
|
@@ -78,10 +79,11 @@ npm run lint # ESLint check
|
|
|
78
79
|
npm run lint:fix # ESLint auto-fix
|
|
79
80
|
npm run format # Prettier auto-format
|
|
80
81
|
npm run typecheck # JSDoc type validation via tsc
|
|
81
|
-
npm run spec # Spec compliance
|
|
82
|
-
npm run spec
|
|
83
|
-
npm run spec
|
|
84
|
-
npm run spec
|
|
82
|
+
npm run spec # Spec compliance (interactive)
|
|
83
|
+
npm run spec all # Full spec check
|
|
84
|
+
npm run spec code # Check code files ≤150 lines
|
|
85
|
+
npm run spec docs # Check doc files ≤500 lines
|
|
86
|
+
npm run spec update # Sync docs from upstream
|
|
85
87
|
```
|
|
86
88
|
|
|
87
89
|
## License
|
package/bin/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* clearstack CLI — scaffold, update, and check spec-compliant projects.
|
|
5
5
|
* Usage:
|
|
6
6
|
* clearstack init [-y] [--static|--fullstack] [--port 3000]
|
|
7
|
-
* clearstack update
|
|
7
|
+
* clearstack update [--force]
|
|
8
8
|
* clearstack check [code|docs|imports|lint|lint es|format|types|audit|all]
|
|
9
9
|
* clearstack → interactive menu
|
|
10
10
|
*/
|
|
@@ -54,7 +54,7 @@ async function run(action) {
|
|
|
54
54
|
await init(PKG_ROOT, { yes, ...flags });
|
|
55
55
|
} else if (action === 'update') {
|
|
56
56
|
const { update } = await import('../lib/update.js');
|
|
57
|
-
await update(PKG_ROOT);
|
|
57
|
+
await update(PKG_ROOT, { force: !!flags.force });
|
|
58
58
|
} else if (action === 'check') {
|
|
59
59
|
const subs = args.filter((a) => a !== cmd && !a.startsWith('-'));
|
|
60
60
|
const { check } = await import('../lib/check.js');
|
package/docs/BUILD_LOG.md
CHANGED
|
@@ -250,3 +250,100 @@ These are the significant corrections:
|
|
|
250
250
|
5. **The spec improves through implementation.** 9 significant corrections were
|
|
251
251
|
made to the spec based on what we learned building the proof. The spec is
|
|
252
252
|
now more accurate than if it had been written in isolation.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## StatiCart: Second Project Validation
|
|
257
|
+
|
|
258
|
+
StatiCart is a full e-commerce platform built on Clearstack — the second
|
|
259
|
+
project built against the spec, validating that the patterns hold beyond
|
|
260
|
+
the original POC. Three phases (catalog, cart, polish) were completed in
|
|
261
|
+
a single LLM session.
|
|
262
|
+
|
|
263
|
+
### Velocity
|
|
264
|
+
|
|
265
|
+
| Metric | Value |
|
|
266
|
+
| --- | --- |
|
|
267
|
+
| Phases completed | 3 of 8 |
|
|
268
|
+
| Components created | 7 (2 atoms, 2 molecules, 2 organisms, 0 templates) |
|
|
269
|
+
| Pages created | 5 (catalog, product-detail, cart, order-success, order-cancelled) |
|
|
270
|
+
| Store models | 2 (Product enumerable, CartState singleton) |
|
|
271
|
+
| Utilities | 4 (formatPrice, setPageMeta, productVariants, statusColors) |
|
|
272
|
+
| Tests | 41 (10 node, 31 browser) |
|
|
273
|
+
| Spec checks | 9/9 passing |
|
|
274
|
+
| Files over 150 lines | 0 (2 hit the limit, both split successfully) |
|
|
275
|
+
|
|
276
|
+
Three phases of a real app in one session. No blocked-on-framework moments.
|
|
277
|
+
Every blocker was a hybrids runtime behavior, not a Clearstack constraint.
|
|
278
|
+
|
|
279
|
+
### What the 150-Line Limit Actually Did
|
|
280
|
+
|
|
281
|
+
The limit forced two splits during this build:
|
|
282
|
+
|
|
283
|
+
1. `shared.css` hit 203 lines after adding page layout styles → split into
|
|
284
|
+
`pages.css`. Later `pages.css` hit 163 → split into `pages-cart.css`.
|
|
285
|
+
2. `product-detail-view.js` hit 154 after adding the image gallery →
|
|
286
|
+
extracted `productVariants.js` utility.
|
|
287
|
+
|
|
288
|
+
Both splits improved the code. The page styles split by concern (catalog
|
|
289
|
+
vs cart) which made each file scannable. The utility extraction made
|
|
290
|
+
`effectivePrice`/`effectiveStock` reusable and independently testable.
|
|
291
|
+
|
|
292
|
+
Without the limit, these would have stayed as 300-line monoliths. The
|
|
293
|
+
LLM would have needed to re-read them on every edit, and the utility
|
|
294
|
+
functions would never have gotten their own test file.
|
|
295
|
+
|
|
296
|
+
### What the Spec Documentation Prevented
|
|
297
|
+
|
|
298
|
+
The Clearstack spec docs prevented several classes of bugs entirely:
|
|
299
|
+
|
|
300
|
+
- **No `../` imports.** The import map alias rule (`#store/`, `#atoms/`)
|
|
301
|
+
was enforced from the first file. Zero refactoring of import paths.
|
|
302
|
+
- **`shadow: false` everywhere.** No time wasted on shadow DOM styling
|
|
303
|
+
issues. Global CSS just worked.
|
|
304
|
+
- **Named event handlers.** No "button doesn't work" debugging from
|
|
305
|
+
inline arrows in nested templates.
|
|
306
|
+
- **`store.connect.get` returns `{}`.** The localStorage pattern was
|
|
307
|
+
correct on first write for CartState.
|
|
308
|
+
- **Template functions over template components.** No host context bugs.
|
|
309
|
+
|
|
310
|
+
### What the Spec Didn't Prevent (New Discoveries)
|
|
311
|
+
|
|
312
|
+
Five new hybrids gotchas were found and documented:
|
|
313
|
+
|
|
314
|
+
1. **Empty arrays throw** — `items: []` needs a prototype item
|
|
315
|
+
2. **`id` is reserved in nested models** — even inside array prototypes
|
|
316
|
+
3. **Enumerable models need `@type {any}`** — tsc can't type `id: true`
|
|
317
|
+
4. **List store descriptors need 3 casts** — descriptor, ready(), and map()
|
|
318
|
+
5. **`list` connector params need cast** — `ModelIdentifier` not plain object
|
|
319
|
+
|
|
320
|
+
All five are hybrids type system friction, not architectural issues. The
|
|
321
|
+
runtime behavior is correct — it's the JSDoc/tsc layer that struggles.
|
|
322
|
+
Each was fixed in under 2 minutes once identified.
|
|
323
|
+
|
|
324
|
+
### Test Runner Discovery
|
|
325
|
+
|
|
326
|
+
`@web/test-runner` doesn't support browser import maps. A custom
|
|
327
|
+
`resolveImport` plugin was needed to map `#prefix/` aliases to absolute
|
|
328
|
+
paths. This is now documented in TESTING.md with the full config pattern.
|
|
329
|
+
|
|
330
|
+
### Framework Reuse Validation
|
|
331
|
+
|
|
332
|
+
The Clearstack scaffolder (`npm run spec update`) worked correctly for
|
|
333
|
+
doc syncing. The config overwrite issue (jsconfig.json, eslint.config.js)
|
|
334
|
+
was found and fixed upstream — `update` now skips existing configs unless
|
|
335
|
+
`--force` is passed. This validates the "dependency, not template" model.
|
|
336
|
+
|
|
337
|
+
### Key Insight
|
|
338
|
+
|
|
339
|
+
The speed came from **not having to make architectural decisions**. The
|
|
340
|
+
spec answered every "where does this go" question before it was asked:
|
|
341
|
+
store models in `src/store/`, atoms vs molecules vs organisms, CSS in
|
|
342
|
+
per-component files registered in `components.css`, pages in `src/pages/`.
|
|
343
|
+
The LLM never had to ask "how should I structure this?" — it read the
|
|
344
|
+
spec and built.
|
|
345
|
+
|
|
346
|
+
This is the core value proposition: **Clearstack trades flexibility for
|
|
347
|
+
velocity.** A team (or LLM) that knows the spec can build without
|
|
348
|
+
discussion. The constraints aren't limitations — they're decisions that
|
|
349
|
+
have already been made.
|
package/docs/CONVENTIONS.md
CHANGED
|
@@ -298,7 +298,7 @@ multiple keys.
|
|
|
298
298
|
|
|
299
299
|
- **One script per domain.** `test`, `spec`, `lint` — not `test:node`,
|
|
300
300
|
`test:browser`, `lint:fix`, `spec:code`, `spec:docs`.
|
|
301
|
-
- **Arguments over aliases.** `pnpm spec
|
|
301
|
+
- **Arguments over aliases.** `pnpm spec code` instead of
|
|
302
302
|
`pnpm spec:code`. The CLI handles routing.
|
|
303
303
|
- **Interactive by default.** Running `pnpm spec` with no arguments shows
|
|
304
304
|
a menu of available actions. Users discover commands by using the tool.
|
|
@@ -370,7 +370,8 @@ npm run spec all
|
|
|
370
370
|
npm run spec code # line counts (code files ≤150)
|
|
371
371
|
npm run spec docs # line counts (doc files ≤500)
|
|
372
372
|
npm run spec imports # import map aliases (no ../)
|
|
373
|
-
npm run spec types #
|
|
373
|
+
npm run spec types # all jsconfigs (auto-discovered)
|
|
374
|
+
npm run spec types frontend # just .configs/jsconfig.json
|
|
374
375
|
npm run spec audit # security audit
|
|
375
376
|
|
|
376
377
|
# Parent keys run all children
|
package/docs/JSDOC_TYPING.md
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
TypeScript's `tsc` compiler validates JSDoc annotations via `--checkJs`,
|
|
14
14
|
giving us compile-time type checking without a build step.
|
|
15
15
|
|
|
16
|
-
A `jsconfig.json` at
|
|
17
|
-
`npm run
|
|
16
|
+
A `jsconfig.json` at `.configs/jsconfig.json` enables `checkJs: true` for
|
|
17
|
+
frontend code. Running `npm run spec types` invokes `tsc` against it.
|
|
18
18
|
|
|
19
19
|
1. Reads all `.js` files in `src/` and `scripts/`
|
|
20
20
|
2. Parses JSDoc annotations as type information
|
|
@@ -24,6 +24,27 @@ A `jsconfig.json` at the project root enables `checkJs: true`. Running
|
|
|
24
24
|
This means `@typedef`, `@type`, `@param`, and `@returns` are not just
|
|
25
25
|
documentation — they are **enforced types**.
|
|
26
26
|
|
|
27
|
+
### Multiple Type Configs
|
|
28
|
+
|
|
29
|
+
The spec checker auto-discovers `jsconfig.json` files in subdirectories.
|
|
30
|
+
If your project has a separate backend (e.g. `api/`) with its own
|
|
31
|
+
dependencies and module resolution, add `api/jsconfig.json`:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
your-project/
|
|
35
|
+
├── .configs/jsconfig.json ← frontend (src/, scripts/)
|
|
36
|
+
└── api/jsconfig.json ← backend (api/, auto-discovered)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm run spec types # runs all discovered jsconfigs
|
|
41
|
+
npm run spec types frontend # just .configs/jsconfig.json
|
|
42
|
+
npm run spec types api # just api/jsconfig.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The child key matches the directory name. Both must pass for
|
|
46
|
+
`npm run spec all` to succeed.
|
|
47
|
+
|
|
27
48
|
---
|
|
28
49
|
|
|
29
50
|
## Component Properties
|
|
@@ -151,4 +172,4 @@ list: async (params) => {
|
|
|
151
172
|
- Keep JSDoc blocks to 3–5 lines. No novels.
|
|
152
173
|
- Use `/** @type {any} */ (expr)` for framework type limitations (e.g.
|
|
153
174
|
`store.pending()` on array results) — document why with a comment.
|
|
154
|
-
- Run `npm run
|
|
175
|
+
- Run `npm run spec types` before committing. Zero errors required.
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -43,7 +43,7 @@ If a `package.json` already exists, Clearstack merges into it — your existing
|
|
|
43
43
|
|
|
44
44
|
```
|
|
45
45
|
your-project/
|
|
46
|
-
├── .configs/ #
|
|
46
|
+
├── .configs/ # ✏️ Scaffolded once — yours to customize
|
|
47
47
|
├── .github/ # CI workflow, PR + issue templates
|
|
48
48
|
├── docs/
|
|
49
49
|
│ ├── clearstack/ # ⟳ Managed — spec docs, synced on update
|
|
@@ -108,14 +108,16 @@ When Clearstack releases a new version:
|
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
npm update @techninja/clearstack # bump the package
|
|
111
|
-
npm run spec
|
|
111
|
+
npm run spec update # sync docs, skip existing configs
|
|
112
|
+
npm run spec update --force # sync docs + overwrite configs
|
|
112
113
|
git diff docs/ .configs/ # review what changed
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
This updates:
|
|
116
117
|
|
|
117
|
-
- `docs/clearstack/*.md` — spec documentation
|
|
118
|
-
- `.configs/*` —
|
|
118
|
+
- `docs/clearstack/*.md` — spec documentation (always overwritten)
|
|
119
|
+
- `.configs/*` — skipped if already exists (your customizations are safe)
|
|
120
|
+
- `.configs/*` with `--force` — overwritten with latest defaults
|
|
119
121
|
|
|
120
122
|
This never touches:
|
|
121
123
|
|
|
@@ -127,12 +129,16 @@ This never touches:
|
|
|
127
129
|
## 8. Spec Compliance
|
|
128
130
|
|
|
129
131
|
```bash
|
|
130
|
-
npm run spec #
|
|
131
|
-
npm run spec
|
|
132
|
-
npm run spec
|
|
133
|
-
npm run
|
|
134
|
-
npm run
|
|
135
|
-
npm run
|
|
132
|
+
npm run spec # interactive menu
|
|
133
|
+
npm run spec all # full check
|
|
134
|
+
npm run spec code # code files ≤150 lines
|
|
135
|
+
npm run spec docs # doc files ≤500 lines
|
|
136
|
+
npm run spec lint # ESLint + Stylelint + Markdown
|
|
137
|
+
npm run spec lint es # ESLint only
|
|
138
|
+
npm run spec format # Prettier
|
|
139
|
+
npm run spec imports # import map aliases
|
|
140
|
+
npm run spec types # JSDoc types (all jsconfigs)
|
|
141
|
+
npm run spec audit # security audit
|
|
136
142
|
npm test # Node + browser tests
|
|
137
143
|
```
|
|
138
144
|
|
|
@@ -153,21 +159,21 @@ SPEC_IGNORE_DIRS=node_modules,src/vendor,.git,.configs
|
|
|
153
159
|
The scaffolded `.github/workflows/spec.yml` runs all checks on every PR:
|
|
154
160
|
|
|
155
161
|
```
|
|
156
|
-
spec
|
|
162
|
+
spec all → lint + format + code + docs + imports + types + audit
|
|
157
163
|
```
|
|
158
164
|
|
|
159
165
|
## Summary
|
|
160
166
|
|
|
161
|
-
| Task | Command
|
|
162
|
-
| --------------------- |
|
|
163
|
-
| Install Clearstack | `npm install -D @techninja/clearstack`
|
|
164
|
-
| Scaffold (fullstack) | `npx clearstack init`
|
|
165
|
-
| Scaffold (static) | `npx clearstack init --static`
|
|
166
|
-
| Install dependencies | `npm install`
|
|
167
|
-
| Start dev server | `npm run dev`
|
|
168
|
-
| Lint + format | `npm run lint
|
|
169
|
-
| Type check | `npm run
|
|
170
|
-
| Run tests | `npm test`
|
|
171
|
-
| Full spec check | `npm run spec`
|
|
172
|
-
| Update spec + configs | `npm run spec
|
|
173
|
-
| Review spec changes | `git diff docs/ .configs/`
|
|
167
|
+
| Task | Command |
|
|
168
|
+
| --------------------- | ------------------------------------------ |
|
|
169
|
+
| Install Clearstack | `npm install -D @techninja/clearstack` |
|
|
170
|
+
| Scaffold (fullstack) | `npx clearstack init` |
|
|
171
|
+
| Scaffold (static) | `npx clearstack init --static` |
|
|
172
|
+
| Install dependencies | `npm install` |
|
|
173
|
+
| Start dev server | `npm run dev` |
|
|
174
|
+
| Lint + format | `npm run spec lint && npm run spec format` |
|
|
175
|
+
| Type check | `npm run spec types` |
|
|
176
|
+
| Run tests | `npm test` |
|
|
177
|
+
| Full spec check | `npm run spec` |
|
|
178
|
+
| Update spec + configs | `npm run spec update` |
|
|
179
|
+
| Review spec changes | `git diff docs/ .configs/` |
|
package/docs/TESTING.md
CHANGED
|
@@ -169,36 +169,31 @@ describe('GET /api/users?schema=true', () => {
|
|
|
169
169
|
|
|
170
170
|
### Component Tests (browser)
|
|
171
171
|
|
|
172
|
-
Using `@web/test-runner` with `@open-wc/testing
|
|
172
|
+
Using `@web/test-runner` with `@open-wc/testing`. Since all components
|
|
173
|
+
use light DOM (`shadow: false`), query the host element directly — not
|
|
174
|
+
`shadowRoot`:
|
|
173
175
|
|
|
174
176
|
```javascript
|
|
175
177
|
// src/components/atoms/app-button/app-button.test.js
|
|
176
|
-
import { fixture,
|
|
178
|
+
import { fixture, expect } from '@open-wc/testing';
|
|
177
179
|
import './app-button.js';
|
|
178
180
|
|
|
179
181
|
describe('app-button', () => {
|
|
180
182
|
it('renders with label', async () => {
|
|
181
|
-
const el = await fixture(
|
|
182
|
-
|
|
183
|
+
const el = await fixture(`<app-button label="Click me"></app-button>`);
|
|
184
|
+
await new Promise((r) => requestAnimationFrame(r));
|
|
185
|
+
const button = el.querySelector('button');
|
|
183
186
|
expect(button.textContent).to.contain('Click me');
|
|
184
187
|
});
|
|
188
|
+
});
|
|
189
|
+
```
|
|
185
190
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const button = el.shadowRoot.querySelector('button');
|
|
189
|
-
expect(button.hasAttribute('disabled')).to.be.true;
|
|
190
|
-
});
|
|
191
|
+
The `requestAnimationFrame` wait gives hybrids time to complete its
|
|
192
|
+
render cycle. For components with async store data, use a longer timeout:
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
el.addEventListener('click', () => {
|
|
196
|
-
clicked = true;
|
|
197
|
-
});
|
|
198
|
-
el.shadowRoot.querySelector('button').click();
|
|
199
|
-
expect(clicked).to.be.true;
|
|
200
|
-
});
|
|
201
|
-
});
|
|
194
|
+
```javascript
|
|
195
|
+
const tick = () => new Promise((r) => setTimeout(r, 100));
|
|
196
|
+
await tick(); // after fixture, before assertions
|
|
202
197
|
```
|
|
203
198
|
|
|
204
199
|
### Store Integration Tests (browser)
|
|
@@ -227,16 +222,54 @@ describe('task-list', () => {
|
|
|
227
222
|
|
|
228
223
|
### web-test-runner.config.js
|
|
229
224
|
|
|
225
|
+
`@web/test-runner` uses `nodeResolve` for bare specifiers like `hybrids`,
|
|
226
|
+
but it does **not** support browser import maps. The `#prefix/` aliases
|
|
227
|
+
from `index.html` won't resolve in tests without a custom plugin:
|
|
228
|
+
|
|
230
229
|
```javascript
|
|
231
230
|
import { playwrightLauncher } from '@web/test-runner-playwright';
|
|
231
|
+
import { dirname, resolve } from 'node:path';
|
|
232
|
+
import { fileURLToPath } from 'node:url';
|
|
233
|
+
|
|
234
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
235
|
+
|
|
236
|
+
const aliases = {
|
|
237
|
+
'#store/': '/src/store/',
|
|
238
|
+
'#utils/': '/src/utils/',
|
|
239
|
+
'#atoms/': '/src/components/atoms/',
|
|
240
|
+
'#molecules/': '/src/components/molecules/',
|
|
241
|
+
'#organisms/': '/src/components/organisms/',
|
|
242
|
+
'#templates/': '/src/components/templates/',
|
|
243
|
+
'#pages/': '/src/pages/',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function importMapPlugin() {
|
|
247
|
+
return {
|
|
248
|
+
name: 'import-map-aliases',
|
|
249
|
+
resolveImport({ source }) {
|
|
250
|
+
for (const [prefix, target] of Object.entries(aliases)) {
|
|
251
|
+
if (source.startsWith(prefix)) return source.replace(prefix, target);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
232
256
|
|
|
233
257
|
export default {
|
|
234
258
|
files: 'src/components/**/*.test.js',
|
|
235
259
|
nodeResolve: true,
|
|
260
|
+
rootDir: ROOT,
|
|
236
261
|
browsers: [playwrightLauncher({ product: 'chromium' })],
|
|
262
|
+
plugins: [importMapPlugin()],
|
|
237
263
|
};
|
|
238
264
|
```
|
|
239
265
|
|
|
266
|
+
Key points:
|
|
267
|
+
|
|
268
|
+
- **`rootDir`** must be the project root so `/src/store/` resolves correctly.
|
|
269
|
+
- **Aliases must match the import map** in `index.html`. If you add a prefix
|
|
270
|
+
there, add it here too.
|
|
271
|
+
- Test files can use `#prefix/` imports just like app code.
|
|
272
|
+
|
|
240
273
|
### package.json Scripts
|
|
241
274
|
|
|
242
275
|
```json
|
package/lib/check.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module lib/check
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
6
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import { checkFileLines, runCmd, countFiles, checkImports } from './spec-utils.js';
|
|
9
9
|
|
|
@@ -51,7 +51,6 @@ export function buildCmds(projectDir) {
|
|
|
51
51
|
stylelint: `${runner} stylelint --config .configs/.stylelintrc.json "src/**/*.css" --fix`,
|
|
52
52
|
prettier: `${runner} prettier --config .configs/.prettierrc --write src scripts`,
|
|
53
53
|
mdlint: `${runner} markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"`,
|
|
54
|
-
types: `${runner} tsc --project .configs/jsconfig.json`,
|
|
55
54
|
audit,
|
|
56
55
|
};
|
|
57
56
|
}
|
|
@@ -59,11 +58,29 @@ export function buildCmds(projectDir) {
|
|
|
59
58
|
|
|
60
59
|
/** @typedef {{ key: string, name: string, parent?: string, run: () => boolean }} Check */
|
|
61
60
|
|
|
62
|
-
/**
|
|
61
|
+
/** Find all jsconfig.json files — main config + any in subdirectories. */
|
|
62
|
+
function findTypeConfigs(dir, runner) {
|
|
63
|
+
const main = resolve(dir, '.configs/jsconfig.json');
|
|
64
|
+
const configs = [];
|
|
65
|
+
if (existsSync(main)) configs.push({ key: 'frontend', label: 'Frontend', path: main });
|
|
66
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
67
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
68
|
+
const p = resolve(dir, entry.name, 'jsconfig.json');
|
|
69
|
+
if (existsSync(p)) configs.push({ key: entry.name, label: entry.name, path: p });
|
|
70
|
+
}
|
|
71
|
+
return configs.map((c) => ({
|
|
72
|
+
key: c.key, name: `JSDoc types — ${c.label}`, parent: 'types',
|
|
73
|
+
run: () => runCmd(`Types (${c.label})`, `${runner} tsc --project ${c.path}`, dir),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Build the unified checks array. Children have a `parent` key.
|
|
78
|
+
* @returns {Check[]} */
|
|
63
79
|
export function buildChecks(dir, cfg, cmds) {
|
|
64
80
|
const js = () => countFiles(dir, ['.js'], cfg.ignore);
|
|
65
81
|
const css = () => countFiles(dir, ['.css'], cfg.ignore);
|
|
66
82
|
const md = () => countFiles(dir, ['.md'], cfg.ignore);
|
|
83
|
+
const runner = detectRunner(dir);
|
|
67
84
|
return [
|
|
68
85
|
{ key: 'es', name: 'ESLint', parent: 'lint',
|
|
69
86
|
run: () => runCmd('ESLint', cmds.lint, dir, `${js()} files`) },
|
|
@@ -79,8 +96,7 @@ export function buildChecks(dir, cfg, cmds) {
|
|
|
79
96
|
run: () => checkFileLines(dir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`) },
|
|
80
97
|
{ key: 'imports', name: 'Import map aliases (no ../ imports)',
|
|
81
98
|
run: () => checkImports(dir, cfg.ignore, 'Import map aliases (no ../ imports)') },
|
|
82
|
-
|
|
83
|
-
run: () => runCmd('JSDoc types', cmds.types, dir, `${js()} files`) },
|
|
99
|
+
...findTypeConfigs(dir, runner),
|
|
84
100
|
{ key: 'audit', name: 'Security audit',
|
|
85
101
|
run: () => runCmd('Security audit', cmds.audit, dir) },
|
|
86
102
|
];
|
package/lib/update.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spec updater — syncs docs and configs from the clearstack package.
|
|
3
|
-
*
|
|
3
|
+
* Docs are overwritten (clearstack-owned). Configs skip existing files
|
|
4
|
+
* to preserve project customizations — use --force to overwrite.
|
|
4
5
|
* @module lib/update
|
|
5
6
|
*/
|
|
6
7
|
|
|
@@ -8,24 +9,27 @@ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from
|
|
|
8
9
|
import { resolve, join } from 'node:path';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* Sync a source directory to a destination
|
|
12
|
+
* Sync a source directory to a destination.
|
|
12
13
|
* @param {string} src
|
|
13
14
|
* @param {string} dest
|
|
14
15
|
* @param {string} label
|
|
16
|
+
* @param {{ skipExisting?: boolean }} [opts]
|
|
15
17
|
* @returns {number} count of updated files
|
|
16
18
|
*/
|
|
17
|
-
function syncDir(src, dest, label) {
|
|
19
|
+
function syncDir(src, dest, label, opts = {}) {
|
|
18
20
|
if (!existsSync(src)) return 0;
|
|
19
21
|
mkdirSync(dest, { recursive: true });
|
|
20
|
-
|
|
21
22
|
const files = readdirSync(src).filter((f) => !f.startsWith('.spec'));
|
|
22
23
|
let updated = 0;
|
|
23
24
|
|
|
24
25
|
for (const file of files) {
|
|
25
|
-
const srcContent = readFileSync(join(src, file), 'utf-8');
|
|
26
26
|
const destPath = join(dest, file);
|
|
27
|
+
if (opts.skipExisting && existsSync(destPath)) {
|
|
28
|
+
console.log(` ⏭ Skipped: ${label}/${file} (exists, use --force to overwrite)`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const srcContent = readFileSync(join(src, file), 'utf-8');
|
|
27
32
|
const destContent = existsSync(destPath) ? readFileSync(destPath, 'utf-8') : '';
|
|
28
|
-
|
|
29
33
|
if (srcContent !== destContent) {
|
|
30
34
|
writeFileSync(destPath, srcContent);
|
|
31
35
|
console.log(` ✓ Updated: ${label}/${file}`);
|
|
@@ -36,25 +40,29 @@ function syncDir(src, dest, label) {
|
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
|
-
* Update spec docs and configs from the clearstack package
|
|
43
|
+
* Update spec docs and configs from the clearstack package.
|
|
40
44
|
* @param {string} pkgRoot - Root of the clearstack package
|
|
45
|
+
* @param {{ force?: boolean }} [opts]
|
|
41
46
|
*/
|
|
42
|
-
export async function update(pkgRoot) {
|
|
47
|
+
export async function update(pkgRoot, opts = {}) {
|
|
43
48
|
const templateShared = resolve(pkgRoot, 'templates/shared');
|
|
44
49
|
let total = 0;
|
|
45
50
|
|
|
46
51
|
console.log('');
|
|
47
52
|
|
|
53
|
+
// Docs: always overwrite (clearstack-owned content)
|
|
48
54
|
total += syncDir(
|
|
49
55
|
resolve(templateShared, 'docs/clearstack'),
|
|
50
56
|
resolve(process.cwd(), 'docs/clearstack'),
|
|
51
57
|
'docs/clearstack',
|
|
52
58
|
);
|
|
53
59
|
|
|
60
|
+
// Configs: skip existing unless --force (project may have customized)
|
|
54
61
|
total += syncDir(
|
|
55
62
|
resolve(templateShared, '.configs'),
|
|
56
63
|
resolve(process.cwd(), '.configs'),
|
|
57
64
|
'.configs',
|
|
65
|
+
{ skipExisting: !opts.force },
|
|
58
66
|
);
|
|
59
67
|
|
|
60
68
|
// Merge .gitignore (append missing lines, never remove user entries)
|
package/package.json
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import { playwrightLauncher } from '@web/test-runner-playwright';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
|
+
|
|
7
|
+
const aliases = {
|
|
8
|
+
'#store/': '/src/store/',
|
|
9
|
+
'#utils/': '/src/utils/',
|
|
10
|
+
'#atoms/': '/src/components/atoms/',
|
|
11
|
+
'#molecules/': '/src/components/molecules/',
|
|
12
|
+
'#organisms/': '/src/components/organisms/',
|
|
13
|
+
'#templates/': '/src/components/templates/',
|
|
14
|
+
'#pages/': '/src/pages/',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Resolve #prefix/ imports for @web/test-runner. */
|
|
18
|
+
function importMapPlugin() {
|
|
19
|
+
return {
|
|
20
|
+
name: 'import-map-aliases',
|
|
21
|
+
resolveImport({ source }) {
|
|
22
|
+
for (const [prefix, target] of Object.entries(aliases)) {
|
|
23
|
+
if (source.startsWith(prefix)) return source.replace(prefix, target);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
2
28
|
|
|
3
29
|
export default {
|
|
4
30
|
files: 'src/components/**/*.test.js',
|
|
5
31
|
nodeResolve: true,
|
|
6
|
-
rootDir:
|
|
32
|
+
rootDir: ROOT,
|
|
7
33
|
browsers: [playwrightLauncher({ product: 'chromium' })],
|
|
34
|
+
plugins: [importMapPlugin()],
|
|
8
35
|
};
|
|
@@ -250,3 +250,100 @@ These are the significant corrections:
|
|
|
250
250
|
5. **The spec improves through implementation.** 9 significant corrections were
|
|
251
251
|
made to the spec based on what we learned building the proof. The spec is
|
|
252
252
|
now more accurate than if it had been written in isolation.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## StatiCart: Second Project Validation
|
|
257
|
+
|
|
258
|
+
StatiCart is a full e-commerce platform built on Clearstack — the second
|
|
259
|
+
project built against the spec, validating that the patterns hold beyond
|
|
260
|
+
the original POC. Three phases (catalog, cart, polish) were completed in
|
|
261
|
+
a single LLM session.
|
|
262
|
+
|
|
263
|
+
### Velocity
|
|
264
|
+
|
|
265
|
+
| Metric | Value |
|
|
266
|
+
| --- | --- |
|
|
267
|
+
| Phases completed | 3 of 8 |
|
|
268
|
+
| Components created | 7 (2 atoms, 2 molecules, 2 organisms, 0 templates) |
|
|
269
|
+
| Pages created | 5 (catalog, product-detail, cart, order-success, order-cancelled) |
|
|
270
|
+
| Store models | 2 (Product enumerable, CartState singleton) |
|
|
271
|
+
| Utilities | 4 (formatPrice, setPageMeta, productVariants, statusColors) |
|
|
272
|
+
| Tests | 41 (10 node, 31 browser) |
|
|
273
|
+
| Spec checks | 9/9 passing |
|
|
274
|
+
| Files over 150 lines | 0 (2 hit the limit, both split successfully) |
|
|
275
|
+
|
|
276
|
+
Three phases of a real app in one session. No blocked-on-framework moments.
|
|
277
|
+
Every blocker was a hybrids runtime behavior, not a Clearstack constraint.
|
|
278
|
+
|
|
279
|
+
### What the 150-Line Limit Actually Did
|
|
280
|
+
|
|
281
|
+
The limit forced two splits during this build:
|
|
282
|
+
|
|
283
|
+
1. `shared.css` hit 203 lines after adding page layout styles → split into
|
|
284
|
+
`pages.css`. Later `pages.css` hit 163 → split into `pages-cart.css`.
|
|
285
|
+
2. `product-detail-view.js` hit 154 after adding the image gallery →
|
|
286
|
+
extracted `productVariants.js` utility.
|
|
287
|
+
|
|
288
|
+
Both splits improved the code. The page styles split by concern (catalog
|
|
289
|
+
vs cart) which made each file scannable. The utility extraction made
|
|
290
|
+
`effectivePrice`/`effectiveStock` reusable and independently testable.
|
|
291
|
+
|
|
292
|
+
Without the limit, these would have stayed as 300-line monoliths. The
|
|
293
|
+
LLM would have needed to re-read them on every edit, and the utility
|
|
294
|
+
functions would never have gotten their own test file.
|
|
295
|
+
|
|
296
|
+
### What the Spec Documentation Prevented
|
|
297
|
+
|
|
298
|
+
The Clearstack spec docs prevented several classes of bugs entirely:
|
|
299
|
+
|
|
300
|
+
- **No `../` imports.** The import map alias rule (`#store/`, `#atoms/`)
|
|
301
|
+
was enforced from the first file. Zero refactoring of import paths.
|
|
302
|
+
- **`shadow: false` everywhere.** No time wasted on shadow DOM styling
|
|
303
|
+
issues. Global CSS just worked.
|
|
304
|
+
- **Named event handlers.** No "button doesn't work" debugging from
|
|
305
|
+
inline arrows in nested templates.
|
|
306
|
+
- **`store.connect.get` returns `{}`.** The localStorage pattern was
|
|
307
|
+
correct on first write for CartState.
|
|
308
|
+
- **Template functions over template components.** No host context bugs.
|
|
309
|
+
|
|
310
|
+
### What the Spec Didn't Prevent (New Discoveries)
|
|
311
|
+
|
|
312
|
+
Five new hybrids gotchas were found and documented:
|
|
313
|
+
|
|
314
|
+
1. **Empty arrays throw** — `items: []` needs a prototype item
|
|
315
|
+
2. **`id` is reserved in nested models** — even inside array prototypes
|
|
316
|
+
3. **Enumerable models need `@type {any}`** — tsc can't type `id: true`
|
|
317
|
+
4. **List store descriptors need 3 casts** — descriptor, ready(), and map()
|
|
318
|
+
5. **`list` connector params need cast** — `ModelIdentifier` not plain object
|
|
319
|
+
|
|
320
|
+
All five are hybrids type system friction, not architectural issues. The
|
|
321
|
+
runtime behavior is correct — it's the JSDoc/tsc layer that struggles.
|
|
322
|
+
Each was fixed in under 2 minutes once identified.
|
|
323
|
+
|
|
324
|
+
### Test Runner Discovery
|
|
325
|
+
|
|
326
|
+
`@web/test-runner` doesn't support browser import maps. A custom
|
|
327
|
+
`resolveImport` plugin was needed to map `#prefix/` aliases to absolute
|
|
328
|
+
paths. This is now documented in TESTING.md with the full config pattern.
|
|
329
|
+
|
|
330
|
+
### Framework Reuse Validation
|
|
331
|
+
|
|
332
|
+
The Clearstack scaffolder (`npm run spec update`) worked correctly for
|
|
333
|
+
doc syncing. The config overwrite issue (jsconfig.json, eslint.config.js)
|
|
334
|
+
was found and fixed upstream — `update` now skips existing configs unless
|
|
335
|
+
`--force` is passed. This validates the "dependency, not template" model.
|
|
336
|
+
|
|
337
|
+
### Key Insight
|
|
338
|
+
|
|
339
|
+
The speed came from **not having to make architectural decisions**. The
|
|
340
|
+
spec answered every "where does this go" question before it was asked:
|
|
341
|
+
store models in `src/store/`, atoms vs molecules vs organisms, CSS in
|
|
342
|
+
per-component files registered in `components.css`, pages in `src/pages/`.
|
|
343
|
+
The LLM never had to ask "how should I structure this?" — it read the
|
|
344
|
+
spec and built.
|
|
345
|
+
|
|
346
|
+
This is the core value proposition: **Clearstack trades flexibility for
|
|
347
|
+
velocity.** A team (or LLM) that knows the spec can build without
|
|
348
|
+
discussion. The constraints aren't limitations — they're decisions that
|
|
349
|
+
have already been made.
|
|
@@ -298,7 +298,7 @@ multiple keys.
|
|
|
298
298
|
|
|
299
299
|
- **One script per domain.** `test`, `spec`, `lint` — not `test:node`,
|
|
300
300
|
`test:browser`, `lint:fix`, `spec:code`, `spec:docs`.
|
|
301
|
-
- **Arguments over aliases.** `pnpm spec
|
|
301
|
+
- **Arguments over aliases.** `pnpm spec code` instead of
|
|
302
302
|
`pnpm spec:code`. The CLI handles routing.
|
|
303
303
|
- **Interactive by default.** Running `pnpm spec` with no arguments shows
|
|
304
304
|
a menu of available actions. Users discover commands by using the tool.
|
|
@@ -370,7 +370,8 @@ npm run spec all
|
|
|
370
370
|
npm run spec code # line counts (code files ≤150)
|
|
371
371
|
npm run spec docs # line counts (doc files ≤500)
|
|
372
372
|
npm run spec imports # import map aliases (no ../)
|
|
373
|
-
npm run spec types #
|
|
373
|
+
npm run spec types # all jsconfigs (auto-discovered)
|
|
374
|
+
npm run spec types frontend # just .configs/jsconfig.json
|
|
374
375
|
npm run spec audit # security audit
|
|
375
376
|
|
|
376
377
|
# Parent keys run all children
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
TypeScript's `tsc` compiler validates JSDoc annotations via `--checkJs`,
|
|
14
14
|
giving us compile-time type checking without a build step.
|
|
15
15
|
|
|
16
|
-
A `jsconfig.json` at
|
|
17
|
-
`npm run
|
|
16
|
+
A `jsconfig.json` at `.configs/jsconfig.json` enables `checkJs: true` for
|
|
17
|
+
frontend code. Running `npm run spec types` invokes `tsc` against it.
|
|
18
18
|
|
|
19
19
|
1. Reads all `.js` files in `src/` and `scripts/`
|
|
20
20
|
2. Parses JSDoc annotations as type information
|
|
@@ -24,6 +24,27 @@ A `jsconfig.json` at the project root enables `checkJs: true`. Running
|
|
|
24
24
|
This means `@typedef`, `@type`, `@param`, and `@returns` are not just
|
|
25
25
|
documentation — they are **enforced types**.
|
|
26
26
|
|
|
27
|
+
### Multiple Type Configs
|
|
28
|
+
|
|
29
|
+
The spec checker auto-discovers `jsconfig.json` files in subdirectories.
|
|
30
|
+
If your project has a separate backend (e.g. `api/`) with its own
|
|
31
|
+
dependencies and module resolution, add `api/jsconfig.json`:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
your-project/
|
|
35
|
+
├── .configs/jsconfig.json ← frontend (src/, scripts/)
|
|
36
|
+
└── api/jsconfig.json ← backend (api/, auto-discovered)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm run spec types # runs all discovered jsconfigs
|
|
41
|
+
npm run spec types frontend # just .configs/jsconfig.json
|
|
42
|
+
npm run spec types api # just api/jsconfig.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The child key matches the directory name. Both must pass for
|
|
46
|
+
`npm run spec all` to succeed.
|
|
47
|
+
|
|
27
48
|
---
|
|
28
49
|
|
|
29
50
|
## Component Properties
|
|
@@ -151,4 +172,4 @@ list: async (params) => {
|
|
|
151
172
|
- Keep JSDoc blocks to 3–5 lines. No novels.
|
|
152
173
|
- Use `/** @type {any} */ (expr)` for framework type limitations (e.g.
|
|
153
174
|
`store.pending()` on array results) — document why with a comment.
|
|
154
|
-
- Run `npm run
|
|
175
|
+
- Run `npm run spec types` before committing. Zero errors required.
|
|
@@ -43,7 +43,7 @@ If a `package.json` already exists, Clearstack merges into it — your existing
|
|
|
43
43
|
|
|
44
44
|
```
|
|
45
45
|
your-project/
|
|
46
|
-
├── .configs/ #
|
|
46
|
+
├── .configs/ # ✏️ Scaffolded once — yours to customize
|
|
47
47
|
├── .github/ # CI workflow, PR + issue templates
|
|
48
48
|
├── docs/
|
|
49
49
|
│ ├── clearstack/ # ⟳ Managed — spec docs, synced on update
|
|
@@ -108,14 +108,16 @@ When Clearstack releases a new version:
|
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
npm update @techninja/clearstack # bump the package
|
|
111
|
-
npm run spec
|
|
111
|
+
npm run spec update # sync docs, skip existing configs
|
|
112
|
+
npm run spec update --force # sync docs + overwrite configs
|
|
112
113
|
git diff docs/ .configs/ # review what changed
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
This updates:
|
|
116
117
|
|
|
117
|
-
- `docs/clearstack/*.md` — spec documentation
|
|
118
|
-
- `.configs/*` —
|
|
118
|
+
- `docs/clearstack/*.md` — spec documentation (always overwritten)
|
|
119
|
+
- `.configs/*` — skipped if already exists (your customizations are safe)
|
|
120
|
+
- `.configs/*` with `--force` — overwritten with latest defaults
|
|
119
121
|
|
|
120
122
|
This never touches:
|
|
121
123
|
|
|
@@ -127,12 +129,16 @@ This never touches:
|
|
|
127
129
|
## 8. Spec Compliance
|
|
128
130
|
|
|
129
131
|
```bash
|
|
130
|
-
npm run spec #
|
|
131
|
-
npm run spec
|
|
132
|
-
npm run spec
|
|
133
|
-
npm run
|
|
134
|
-
npm run
|
|
135
|
-
npm run
|
|
132
|
+
npm run spec # interactive menu
|
|
133
|
+
npm run spec all # full check
|
|
134
|
+
npm run spec code # code files ≤150 lines
|
|
135
|
+
npm run spec docs # doc files ≤500 lines
|
|
136
|
+
npm run spec lint # ESLint + Stylelint + Markdown
|
|
137
|
+
npm run spec lint es # ESLint only
|
|
138
|
+
npm run spec format # Prettier
|
|
139
|
+
npm run spec imports # import map aliases
|
|
140
|
+
npm run spec types # JSDoc types (all jsconfigs)
|
|
141
|
+
npm run spec audit # security audit
|
|
136
142
|
npm test # Node + browser tests
|
|
137
143
|
```
|
|
138
144
|
|
|
@@ -153,21 +159,21 @@ SPEC_IGNORE_DIRS=node_modules,src/vendor,.git,.configs
|
|
|
153
159
|
The scaffolded `.github/workflows/spec.yml` runs all checks on every PR:
|
|
154
160
|
|
|
155
161
|
```
|
|
156
|
-
spec
|
|
162
|
+
spec all → lint + format + code + docs + imports + types + audit
|
|
157
163
|
```
|
|
158
164
|
|
|
159
165
|
## Summary
|
|
160
166
|
|
|
161
|
-
| Task | Command
|
|
162
|
-
| --------------------- |
|
|
163
|
-
| Install Clearstack | `npm install -D @techninja/clearstack`
|
|
164
|
-
| Scaffold (fullstack) | `npx clearstack init`
|
|
165
|
-
| Scaffold (static) | `npx clearstack init --static`
|
|
166
|
-
| Install dependencies | `npm install`
|
|
167
|
-
| Start dev server | `npm run dev`
|
|
168
|
-
| Lint + format | `npm run lint
|
|
169
|
-
| Type check | `npm run
|
|
170
|
-
| Run tests | `npm test`
|
|
171
|
-
| Full spec check | `npm run spec`
|
|
172
|
-
| Update spec + configs | `npm run spec
|
|
173
|
-
| Review spec changes | `git diff docs/ .configs/`
|
|
167
|
+
| Task | Command |
|
|
168
|
+
| --------------------- | ------------------------------------------ |
|
|
169
|
+
| Install Clearstack | `npm install -D @techninja/clearstack` |
|
|
170
|
+
| Scaffold (fullstack) | `npx clearstack init` |
|
|
171
|
+
| Scaffold (static) | `npx clearstack init --static` |
|
|
172
|
+
| Install dependencies | `npm install` |
|
|
173
|
+
| Start dev server | `npm run dev` |
|
|
174
|
+
| Lint + format | `npm run spec lint && npm run spec format` |
|
|
175
|
+
| Type check | `npm run spec types` |
|
|
176
|
+
| Run tests | `npm test` |
|
|
177
|
+
| Full spec check | `npm run spec` |
|
|
178
|
+
| Update spec + configs | `npm run spec update` |
|
|
179
|
+
| Review spec changes | `git diff docs/ .configs/` |
|
|
@@ -169,36 +169,31 @@ describe('GET /api/users?schema=true', () => {
|
|
|
169
169
|
|
|
170
170
|
### Component Tests (browser)
|
|
171
171
|
|
|
172
|
-
Using `@web/test-runner` with `@open-wc/testing
|
|
172
|
+
Using `@web/test-runner` with `@open-wc/testing`. Since all components
|
|
173
|
+
use light DOM (`shadow: false`), query the host element directly — not
|
|
174
|
+
`shadowRoot`:
|
|
173
175
|
|
|
174
176
|
```javascript
|
|
175
177
|
// src/components/atoms/app-button/app-button.test.js
|
|
176
|
-
import { fixture,
|
|
178
|
+
import { fixture, expect } from '@open-wc/testing';
|
|
177
179
|
import './app-button.js';
|
|
178
180
|
|
|
179
181
|
describe('app-button', () => {
|
|
180
182
|
it('renders with label', async () => {
|
|
181
|
-
const el = await fixture(
|
|
182
|
-
|
|
183
|
+
const el = await fixture(`<app-button label="Click me"></app-button>`);
|
|
184
|
+
await new Promise((r) => requestAnimationFrame(r));
|
|
185
|
+
const button = el.querySelector('button');
|
|
183
186
|
expect(button.textContent).to.contain('Click me');
|
|
184
187
|
});
|
|
188
|
+
});
|
|
189
|
+
```
|
|
185
190
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const button = el.shadowRoot.querySelector('button');
|
|
189
|
-
expect(button.hasAttribute('disabled')).to.be.true;
|
|
190
|
-
});
|
|
191
|
+
The `requestAnimationFrame` wait gives hybrids time to complete its
|
|
192
|
+
render cycle. For components with async store data, use a longer timeout:
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
el.addEventListener('click', () => {
|
|
196
|
-
clicked = true;
|
|
197
|
-
});
|
|
198
|
-
el.shadowRoot.querySelector('button').click();
|
|
199
|
-
expect(clicked).to.be.true;
|
|
200
|
-
});
|
|
201
|
-
});
|
|
194
|
+
```javascript
|
|
195
|
+
const tick = () => new Promise((r) => setTimeout(r, 100));
|
|
196
|
+
await tick(); // after fixture, before assertions
|
|
202
197
|
```
|
|
203
198
|
|
|
204
199
|
### Store Integration Tests (browser)
|
|
@@ -227,16 +222,54 @@ describe('task-list', () => {
|
|
|
227
222
|
|
|
228
223
|
### web-test-runner.config.js
|
|
229
224
|
|
|
225
|
+
`@web/test-runner` uses `nodeResolve` for bare specifiers like `hybrids`,
|
|
226
|
+
but it does **not** support browser import maps. The `#prefix/` aliases
|
|
227
|
+
from `index.html` won't resolve in tests without a custom plugin:
|
|
228
|
+
|
|
230
229
|
```javascript
|
|
231
230
|
import { playwrightLauncher } from '@web/test-runner-playwright';
|
|
231
|
+
import { dirname, resolve } from 'node:path';
|
|
232
|
+
import { fileURLToPath } from 'node:url';
|
|
233
|
+
|
|
234
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
235
|
+
|
|
236
|
+
const aliases = {
|
|
237
|
+
'#store/': '/src/store/',
|
|
238
|
+
'#utils/': '/src/utils/',
|
|
239
|
+
'#atoms/': '/src/components/atoms/',
|
|
240
|
+
'#molecules/': '/src/components/molecules/',
|
|
241
|
+
'#organisms/': '/src/components/organisms/',
|
|
242
|
+
'#templates/': '/src/components/templates/',
|
|
243
|
+
'#pages/': '/src/pages/',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function importMapPlugin() {
|
|
247
|
+
return {
|
|
248
|
+
name: 'import-map-aliases',
|
|
249
|
+
resolveImport({ source }) {
|
|
250
|
+
for (const [prefix, target] of Object.entries(aliases)) {
|
|
251
|
+
if (source.startsWith(prefix)) return source.replace(prefix, target);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
232
256
|
|
|
233
257
|
export default {
|
|
234
258
|
files: 'src/components/**/*.test.js',
|
|
235
259
|
nodeResolve: true,
|
|
260
|
+
rootDir: ROOT,
|
|
236
261
|
browsers: [playwrightLauncher({ product: 'chromium' })],
|
|
262
|
+
plugins: [importMapPlugin()],
|
|
237
263
|
};
|
|
238
264
|
```
|
|
239
265
|
|
|
266
|
+
Key points:
|
|
267
|
+
|
|
268
|
+
- **`rootDir`** must be the project root so `/src/store/` resolves correctly.
|
|
269
|
+
- **Aliases must match the import map** in `index.html`. If you add a prefix
|
|
270
|
+
there, add it here too.
|
|
271
|
+
- Test files can use `#prefix/` imports just like app code.
|
|
272
|
+
|
|
240
273
|
### package.json Scripts
|
|
241
274
|
|
|
242
275
|
```json
|