@techninja/clearstack 0.3.16 → 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/docs/BUILD_LOG.md +97 -0
- package/docs/CONVENTIONS.md +2 -1
- package/docs/JSDOC_TYPING.md +24 -3
- package/docs/QUICKSTART.md +2 -2
- package/docs/TESTING.md +52 -19
- package/lib/check.js +21 -5
- 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 +2 -1
- package/templates/shared/docs/clearstack/JSDOC_TYPING.md +24 -3
- package/templates/shared/docs/clearstack/QUICKSTART.md +2 -2
- package/templates/shared/docs/clearstack/TESTING.md +52 -19
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
|
@@ -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
|
@@ -137,7 +137,7 @@ npm run spec lint # ESLint + Stylelint + Markdown
|
|
|
137
137
|
npm run spec lint es # ESLint only
|
|
138
138
|
npm run spec format # Prettier
|
|
139
139
|
npm run spec imports # import map aliases
|
|
140
|
-
npm run spec types # JSDoc
|
|
140
|
+
npm run spec types # JSDoc types (all jsconfigs)
|
|
141
141
|
npm run spec audit # security audit
|
|
142
142
|
npm test # Node + browser tests
|
|
143
143
|
```
|
|
@@ -172,7 +172,7 @@ spec all → lint + format + code + docs + imports + types + audit
|
|
|
172
172
|
| Install dependencies | `npm install` |
|
|
173
173
|
| Start dev server | `npm run dev` |
|
|
174
174
|
| Lint + format | `npm run spec lint && npm run spec format` |
|
|
175
|
-
| Type check | `npm run
|
|
175
|
+
| Type check | `npm run spec types` |
|
|
176
176
|
| Run tests | `npm test` |
|
|
177
177
|
| Full spec check | `npm run spec` |
|
|
178
178
|
| Update spec + configs | `npm run spec update` |
|
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/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.
|
|
@@ -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.
|
|
@@ -137,7 +137,7 @@ npm run spec lint # ESLint + Stylelint + Markdown
|
|
|
137
137
|
npm run spec lint es # ESLint only
|
|
138
138
|
npm run spec format # Prettier
|
|
139
139
|
npm run spec imports # import map aliases
|
|
140
|
-
npm run spec types # JSDoc
|
|
140
|
+
npm run spec types # JSDoc types (all jsconfigs)
|
|
141
141
|
npm run spec audit # security audit
|
|
142
142
|
npm test # Node + browser tests
|
|
143
143
|
```
|
|
@@ -172,7 +172,7 @@ spec all → lint + format + code + docs + imports + types + audit
|
|
|
172
172
|
| Install dependencies | `npm install` |
|
|
173
173
|
| Start dev server | `npm run dev` |
|
|
174
174
|
| Lint + format | `npm run spec lint && npm run spec format` |
|
|
175
|
-
| Type check | `npm run
|
|
175
|
+
| Type check | `npm run spec types` |
|
|
176
176
|
| Run tests | `npm test` |
|
|
177
177
|
| Full spec check | `npm run spec` |
|
|
178
178
|
| Update spec + configs | `npm run spec update` |
|
|
@@ -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
|