@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 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.
@@ -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 # JSDoc types (tsc --checkJs)
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 the project root enables `checkJs: true`. Running
17
- `npm run typecheck` invokes `tsc --project jsconfig.json` which:
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 typecheck` before committing. Zero errors required.
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 type validation
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 typecheck` |
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, html, expect } from '@open-wc/testing';
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(html`<app-button label="Click me"></app-button>`);
182
- const button = el.shadowRoot.querySelector('button');
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
- it('reflects disabled attribute', async () => {
187
- const el = await fixture(html`<app-button disabled></app-button>`);
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
- it('dispatches click event', async () => {
193
- const el = await fixture(html`<app-button label="Go"></app-button>`);
194
- let clicked = false;
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
- /** Build the unified checks array. Children have a `parent` key. */
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
- { key: 'types', name: 'JSDoc types (tsc --checkJs)',
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -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 # JSDoc types (tsc --checkJs)
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 the project root enables `checkJs: true`. Running
17
- `npm run typecheck` invokes `tsc --project jsconfig.json` which:
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 typecheck` before committing. Zero errors required.
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 type validation
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 typecheck` |
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, html, expect } from '@open-wc/testing';
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(html`<app-button label="Click me"></app-button>`);
182
- const button = el.shadowRoot.querySelector('button');
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
- it('reflects disabled attribute', async () => {
187
- const el = await fixture(html`<app-button disabled></app-button>`);
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
- it('dispatches click event', async () => {
193
- const el = await fixture(html`<app-button label="Go"></app-button>`);
194
- let clicked = false;
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