@techninja/clearstack 0.3.28 → 0.3.30

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.
@@ -102,21 +102,14 @@ default values, so the model initializes cleanly:
102
102
 
103
103
  ```javascript
104
104
  [store.connect]: {
105
- // ✅ GOOD — returns empty object, hybrids merges with defaults
106
105
  get: () => {
107
106
  const raw = localStorage.getItem('appState');
108
- return raw ? JSON.parse(raw) : {};
107
+ return raw ? JSON.parse(raw) : {}; // ✅ — {} merges with defaults
108
+ // return raw ? JSON.parse(raw) : undefined; // ❌ — triggers error state
109
109
  },
110
- // ❌ BAD — undefined triggers error state
111
- // get: () => {
112
- // const raw = localStorage.getItem('appState');
113
- // return raw ? JSON.parse(raw) : undefined;
114
- // },
115
110
  }
116
111
  ```
117
112
 
118
- This applies to all localStorage-backed singletons (`AppState`, `UserPrefs`).
119
-
120
113
  Consume in any component:
121
114
 
122
115
  ```javascript
@@ -227,56 +220,28 @@ non-array during the pending phase.
227
220
  Always guard with `Array.isArray()` before calling array methods:
228
221
 
229
222
  ```javascript
230
- export default define({
231
- tag: 'my-view',
232
- items: {
233
- value: [],
234
- connect: (host, _key, invalidate) => {
235
- fetchItems().then((list) => {
236
- host.items = list;
237
- invalidate();
238
- });
239
- },
240
- },
241
- render: ({ items }) => html`
242
- // ❌ BAD — items may not be an array yet when render fires
243
- ${items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)} // ✅ GOOD — guard
244
- before calling array methods
245
- ${Array.isArray(items) && items.length > 0
246
- ? items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)
247
- : html``}
248
- `,
249
- });
223
+ // BAD — items may not be an array yet when render fires
224
+ ${items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)}
225
+
226
+ // ✅ GOOD — guard before calling array methods
227
+ ${Array.isArray(items) && items.length > 0
228
+ ? items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)
229
+ : html``}
250
230
  ```
251
231
 
252
- This applies to any property with `value: []` and an async `connect`.
253
- The `connect` callback sets the value and calls `invalidate()`, but the
254
- first render happens before that resolves.
232
+ The first render fires before `connect`'s async callback resolves.
255
233
 
256
234
  #### Async Init with Child Component Props
257
235
 
258
- When a parent loads data during `connect` and passes it to a child via
259
- template props, the child may never see the change. Hybrids' change
260
- detection requires observing old → new. If the prop is set before the
261
- child mounts, there's no "old" value to compare against.
236
+ When a parent sets a child prop during `connect`, the child may never
237
+ see the change (no old new transition). Defer to after first render:
262
238
 
263
239
  ```javascript
264
- // ❌ BAD — resultCount set during connect, before child mounts
265
- _init: {
266
- value: false,
267
- connect: (host, _key, invalidate) => {
268
- loadData(host).then(() => {
269
- host.resultCount = 14;
270
- invalidate(); // child mounts with 14, never sees a change
271
- });
272
- },
240
+ // ❌ BAD — child mounts with 14, never sees a change
241
+ connect: (host, _key, invalidate) => {
242
+ loadData(host).then(() => { host.resultCount = 14; invalidate(); });
273
243
  },
274
- ```
275
-
276
- The fix: defer the prop update to after the first render frame so the
277
- child is mounted and can observe the transition:
278
244
 
279
- ```javascript
280
245
  // ✅ GOOD — child mounts with default (0), then sees 0 → 14
281
246
  connect: (host, _key, invalidate) => {
282
247
  loadData(host).then(() => {
@@ -286,10 +251,6 @@ connect: (host, _key, invalidate) => {
286
251
  },
287
252
  ```
288
253
 
289
- Pre-populate external caches during `loadData` so the child has data
290
- ready when the deferred update triggers. The 0 → 14 transition happens
291
- within a single frame — no flicker.
292
-
293
254
  ---
294
255
 
295
256
  ## Routing
@@ -346,40 +307,21 @@ export default define({
346
307
 
347
308
  #### Router Property Cache: Same-Value Writes Are No-Ops
348
309
 
349
- The router caches component property values across navigations and page
350
- reloads. When a view reconnects, its properties are restored from cache
351
- **before** `connect` callbacks run.
352
-
353
- This means if a `connect` callback loads data asynchronously and then sets
354
- a property to the same value the cache already holds, hybrids sees no
355
- change and skips the re-render:
310
+ The router restores cached property values on reconnect. If `connect`
311
+ sets a property to the same value the cache holds, hybrids skips re-render.
312
+ Reset to a sentinel first:
356
313
 
357
314
  ```javascript
358
- // ❌ BAD — if router cache already has resultCount=22,
359
- // setting it to 22 again is a no-op. No re-render happens.
360
- connect: (host, _key, invalidate) => {
361
- loadFromDB(host.userId).then((data) => {
362
- populateMemoryCache(data); // side effect: fills an external object
363
- host.resultCount = data.length; // may equal cached value → no re-render
364
- invalidate();
365
- });
366
- },
315
+ // ❌ BAD — may equal cached value no re-render
316
+ host.resultCount = data.length;
367
317
 
368
- // ✅ GOOD — reset to a sentinel value first, then set the real value.
369
- // Hybrids sees 0 → 22, triggers re-render after data is loaded.
370
- connect: (host, _key, invalidate) => {
371
- loadFromDB(host.userId).then((data) => {
372
- populateMemoryCache(data);
373
- host.resultCount = 0; // force a change
374
- host.resultCount = data.length; // now hybrids sees a real change
375
- invalidate();
376
- });
377
- },
318
+ // ✅ GOOD — force a change so hybrids sees 0 22
319
+ host.resultCount = 0;
320
+ host.resultCount = data.length;
378
321
  ```
379
322
 
380
- This is especially important when render depends on external mutable state
381
- (e.g. an in-memory cache object) that the property change is meant to
382
- signal. Without the reset, the component renders with stale external state.
323
+ Important when render depends on external mutable state that the property
324
+ change is meant to signal.
383
325
 
384
326
  #### Router Cache Poisons Boolean Flags Across Reconnects
385
327
 
package/lib/update.js CHANGED
@@ -14,7 +14,7 @@ import { detectPlatforms, updatePlatform } from './platform.js';
14
14
  * @param {string} src
15
15
  * @param {string} dest
16
16
  * @param {string} label
17
- * @param {{ skipExisting?: boolean }} [opts]
17
+ * @param {{ skipExisting?: boolean, onlyExisting?: boolean }} [opts]
18
18
  * @returns {number} count of updated files
19
19
  */
20
20
  function syncDir(src, dest, label, opts = {}) {
@@ -25,6 +25,7 @@ function syncDir(src, dest, label, opts = {}) {
25
25
 
26
26
  for (const file of files) {
27
27
  const destPath = join(dest, file);
28
+ if (opts.onlyExisting && !existsSync(destPath)) continue;
28
29
  if (opts.skipExisting && existsSync(destPath)) {
29
30
  console.log(` ⏭ Skipped: ${label}/${file} (exists, use --force to overwrite)`);
30
31
  continue;
@@ -82,6 +83,14 @@ export async function update(pkgRoot, opts = {}) {
82
83
  }
83
84
  }
84
85
 
86
+ // Scripts: sync clearstack-owned scripts (only update existing files)
87
+ total += syncDir(
88
+ resolve(templateShared, 'scripts'),
89
+ resolve(process.cwd(), 'scripts'),
90
+ 'scripts',
91
+ { onlyExisting: true },
92
+ );
93
+
85
94
  // Write spec version marker
86
95
  const pkg = JSON.parse(readFileSync(resolve(pkgRoot, 'package.json'), 'utf-8'));
87
96
  const versionPath = resolve(process.cwd(), 'docs/clearstack/.specversion');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -102,21 +102,14 @@ default values, so the model initializes cleanly:
102
102
 
103
103
  ```javascript
104
104
  [store.connect]: {
105
- // ✅ GOOD — returns empty object, hybrids merges with defaults
106
105
  get: () => {
107
106
  const raw = localStorage.getItem('appState');
108
- return raw ? JSON.parse(raw) : {};
107
+ return raw ? JSON.parse(raw) : {}; // ✅ — {} merges with defaults
108
+ // return raw ? JSON.parse(raw) : undefined; // ❌ — triggers error state
109
109
  },
110
- // ❌ BAD — undefined triggers error state
111
- // get: () => {
112
- // const raw = localStorage.getItem('appState');
113
- // return raw ? JSON.parse(raw) : undefined;
114
- // },
115
110
  }
116
111
  ```
117
112
 
118
- This applies to all localStorage-backed singletons (`AppState`, `UserPrefs`).
119
-
120
113
  Consume in any component:
121
114
 
122
115
  ```javascript
@@ -227,56 +220,28 @@ non-array during the pending phase.
227
220
  Always guard with `Array.isArray()` before calling array methods:
228
221
 
229
222
  ```javascript
230
- export default define({
231
- tag: 'my-view',
232
- items: {
233
- value: [],
234
- connect: (host, _key, invalidate) => {
235
- fetchItems().then((list) => {
236
- host.items = list;
237
- invalidate();
238
- });
239
- },
240
- },
241
- render: ({ items }) => html`
242
- // ❌ BAD — items may not be an array yet when render fires
243
- ${items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)} // ✅ GOOD — guard
244
- before calling array methods
245
- ${Array.isArray(items) && items.length > 0
246
- ? items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)
247
- : html``}
248
- `,
249
- });
223
+ // BAD — items may not be an array yet when render fires
224
+ ${items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)}
225
+
226
+ // ✅ GOOD — guard before calling array methods
227
+ ${Array.isArray(items) && items.length > 0
228
+ ? items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)
229
+ : html``}
250
230
  ```
251
231
 
252
- This applies to any property with `value: []` and an async `connect`.
253
- The `connect` callback sets the value and calls `invalidate()`, but the
254
- first render happens before that resolves.
232
+ The first render fires before `connect`'s async callback resolves.
255
233
 
256
234
  #### Async Init with Child Component Props
257
235
 
258
- When a parent loads data during `connect` and passes it to a child via
259
- template props, the child may never see the change. Hybrids' change
260
- detection requires observing old → new. If the prop is set before the
261
- child mounts, there's no "old" value to compare against.
236
+ When a parent sets a child prop during `connect`, the child may never
237
+ see the change (no old new transition). Defer to after first render:
262
238
 
263
239
  ```javascript
264
- // ❌ BAD — resultCount set during connect, before child mounts
265
- _init: {
266
- value: false,
267
- connect: (host, _key, invalidate) => {
268
- loadData(host).then(() => {
269
- host.resultCount = 14;
270
- invalidate(); // child mounts with 14, never sees a change
271
- });
272
- },
240
+ // ❌ BAD — child mounts with 14, never sees a change
241
+ connect: (host, _key, invalidate) => {
242
+ loadData(host).then(() => { host.resultCount = 14; invalidate(); });
273
243
  },
274
- ```
275
-
276
- The fix: defer the prop update to after the first render frame so the
277
- child is mounted and can observe the transition:
278
244
 
279
- ```javascript
280
245
  // ✅ GOOD — child mounts with default (0), then sees 0 → 14
281
246
  connect: (host, _key, invalidate) => {
282
247
  loadData(host).then(() => {
@@ -286,10 +251,6 @@ connect: (host, _key, invalidate) => {
286
251
  },
287
252
  ```
288
253
 
289
- Pre-populate external caches during `loadData` so the child has data
290
- ready when the deferred update triggers. The 0 → 14 transition happens
291
- within a single frame — no flicker.
292
-
293
254
  ---
294
255
 
295
256
  ## Routing
@@ -346,40 +307,21 @@ export default define({
346
307
 
347
308
  #### Router Property Cache: Same-Value Writes Are No-Ops
348
309
 
349
- The router caches component property values across navigations and page
350
- reloads. When a view reconnects, its properties are restored from cache
351
- **before** `connect` callbacks run.
352
-
353
- This means if a `connect` callback loads data asynchronously and then sets
354
- a property to the same value the cache already holds, hybrids sees no
355
- change and skips the re-render:
310
+ The router restores cached property values on reconnect. If `connect`
311
+ sets a property to the same value the cache holds, hybrids skips re-render.
312
+ Reset to a sentinel first:
356
313
 
357
314
  ```javascript
358
- // ❌ BAD — if router cache already has resultCount=22,
359
- // setting it to 22 again is a no-op. No re-render happens.
360
- connect: (host, _key, invalidate) => {
361
- loadFromDB(host.userId).then((data) => {
362
- populateMemoryCache(data); // side effect: fills an external object
363
- host.resultCount = data.length; // may equal cached value → no re-render
364
- invalidate();
365
- });
366
- },
315
+ // ❌ BAD — may equal cached value no re-render
316
+ host.resultCount = data.length;
367
317
 
368
- // ✅ GOOD — reset to a sentinel value first, then set the real value.
369
- // Hybrids sees 0 → 22, triggers re-render after data is loaded.
370
- connect: (host, _key, invalidate) => {
371
- loadFromDB(host.userId).then((data) => {
372
- populateMemoryCache(data);
373
- host.resultCount = 0; // force a change
374
- host.resultCount = data.length; // now hybrids sees a real change
375
- invalidate();
376
- });
377
- },
318
+ // ✅ GOOD — force a change so hybrids sees 0 22
319
+ host.resultCount = 0;
320
+ host.resultCount = data.length;
378
321
  ```
379
322
 
380
- This is especially important when render depends on external mutable state
381
- (e.g. an in-memory cache object) that the property change is meant to
382
- signal. Without the reset, the component renders with stale external state.
323
+ Important when render depends on external mutable state that the property
324
+ change is meant to signal.
383
325
 
384
326
  #### Router Cache Poisons Boolean Flags Across Reconnects
385
327