sygnal 6.4.0 → 6.6.0

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/astro/index.cjs.js +398 -25
  3. package/dist/astro/index.cjs.js.map +1 -1
  4. package/dist/astro/index.mjs +398 -25
  5. package/dist/astro/index.mjs.map +1 -1
  6. package/dist/astro/server.cjs.js +3 -2
  7. package/dist/astro/server.cjs.js.map +1 -1
  8. package/dist/astro/server.mjs +3 -2
  9. package/dist/astro/server.mjs.map +1 -1
  10. package/dist/devtools.cjs.js +141 -3
  11. package/dist/devtools.cjs.js.map +1 -1
  12. package/dist/devtools.esm.js +141 -4
  13. package/dist/devtools.esm.js.map +1 -1
  14. package/dist/diagnostics.cjs.js +140 -21
  15. package/dist/diagnostics.cjs.js.map +1 -1
  16. package/dist/diagnostics.esm.js +140 -21
  17. package/dist/diagnostics.esm.js.map +1 -1
  18. package/dist/index.cjs.js +54 -24
  19. package/dist/index.cjs.js.map +1 -1
  20. package/dist/index.d.ts +139 -16
  21. package/dist/index.esm.js +54 -24
  22. package/dist/index.esm.js.map +1 -1
  23. package/dist/sygnal.min.js +1 -1
  24. package/dist/sygnal.min.js.map +1 -1
  25. package/dist/vike/config/package.json +1 -1
  26. package/dist/vike/onRenderClient.cjs.js +63 -43
  27. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  28. package/dist/vike/onRenderClient.mjs +63 -43
  29. package/dist/vike/onRenderClient.mjs.map +1 -1
  30. package/dist/vike/onRenderHtml.cjs.js +21 -25
  31. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  32. package/dist/vike/onRenderHtml.mjs +21 -25
  33. package/dist/vike/onRenderHtml.mjs.map +1 -1
  34. package/dist/vite/plugin.cjs.js +398 -25
  35. package/dist/vite/plugin.cjs.js.map +1 -1
  36. package/dist/vite/plugin.mjs +398 -25
  37. package/dist/vite/plugin.mjs.map +1 -1
  38. package/llms.txt +3 -3
  39. package/package.json +1 -1
  40. package/src/core/cell.ts +25 -7
  41. package/src/core/hooks.ts +6 -0
  42. package/src/core/hosts/collection.ts +6 -11
  43. package/src/core/hosts/switchable.ts +2 -2
  44. package/src/core/instance.ts +2 -2
  45. package/src/core/view.ts +2 -0
  46. package/src/cycle/dom/utils.ts +4 -2
  47. package/src/devtools.d.ts +13 -0
  48. package/src/devtools.ts +3 -0
  49. package/src/extra/devGraph.ts +111 -0
  50. package/src/extra/devMcp.ts +1 -1
  51. package/src/extra/devtools.ts +18 -0
  52. package/src/extra/diagnostics/checks/collections.ts +31 -25
  53. package/src/extra/diagnostics/checks/index.ts +1 -1
  54. package/src/extra/diagnostics/checks/inspect.ts +68 -6
  55. package/src/extra/diagnostics/checks/next.ts +22 -0
  56. package/src/extra/diagnostics/checks/public.d.ts +121 -12
  57. package/src/extra/diagnostics/codes.ts +10 -0
  58. package/src/index.d.ts +19 -3
  59. package/src/vike/onRenderClient.ts +44 -46
  60. package/src/vike/onRenderHtml.ts +5 -26
  61. package/src/vike/shell.ts +33 -0
  62. package/src/vite/graph.ts +247 -0
  63. package/src/vite/mcp.ts +3 -3
  64. package/src/vite/plugin.d.ts +12 -0
  65. package/src/vite/plugin.ts +136 -22
@@ -143,6 +143,9 @@ const CODE_TITLES = {
143
143
  SYG127: 'Behavior collision or unresolvable uses entry',
144
144
  SYG128: 'Duplicate control key',
145
145
  SYG129: "CHILD.select() of a component this one doesn't render",
146
+ // PLAN-10 X-1 (D512): graph-wide checks, static only (sygnal-check)
147
+ SYG170: 'Context field read with no provider',
148
+ SYG171: 'PARENT sink no parent reads',
146
149
  SYG112: 'Reply action has no model entry',
147
150
  SYG130: 'href() names no route or leaves out a param',
148
151
  SYG131: 'Route params the pattern does not use',
@@ -272,6 +275,8 @@ const CODE_TITLES = {
272
275
  // checks/next.ts and sygnal-check: a component reference that is not a function (an import of
273
276
  // a name the module doesn't export is undefined under Vitest)
274
277
  SYG427: 'CHILD.select() of something that is not a component',
278
+ // 6.5.0 (checks/next.ts): state="a.b" / from="a.b" is a path; the parent state has a literal "a.b" key
279
+ SYG428: 'A state path that names a key containing a dot',
275
280
  // PLAN-9 (D470): checks/next.ts; dev entry
276
281
  SYG453: 'Two children with the same identity share one instance',
277
282
  SYG601: "Retired in 6.0: Missing or invalid sources",
@@ -414,6 +419,9 @@ const DEV_CODE_SEVERITY = {
414
419
  SYG128: 'error',
415
420
  // PLAN-4 4-G2: static only (sygnal-check); listed so the entry knows its title
416
421
  SYG129: 'warn',
422
+ // PLAN-10 X-1: static only (sygnal-check)
423
+ SYG170: 'warn',
424
+ SYG171: 'warn',
417
425
  SYG221: 'error',
418
426
  // PLAN-4 GS-4 (state.ts)
419
427
  SYG222: 'warn',
@@ -480,6 +488,8 @@ const DEV_CODE_SEVERITY = {
480
488
  SYG426: 'warn',
481
489
  // checks/next.ts: CHILD.select(undefined), usually a missing export
482
490
  SYG427: 'error',
491
+ // checks/next.ts: a path designator that 6.4 read as one key with a dot
492
+ SYG428: 'warn',
483
493
  // PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
484
494
  SYG453: 'warn',
485
495
  SYG612: 'error',
@@ -1153,25 +1163,44 @@ function checkCollection(component, el) {
1153
1163
  state = component.addCalculated(state);
1154
1164
  }
1155
1165
  catch (_) { }
1156
- let problem;
1157
- let fix = `Pass the name of an array field in ${name}'s state, e.g. <Collection of={${ofName}} from="items" />, and initialize that field to [] in initialState`;
1158
- // Every defined `from` (string field, lens object, or an invalid value) is reported by
1159
- // the core itself (SYG401/SYG412, retrofitted in 1E) in every diagnostics mode (D22), so
1160
- // this check only covers a missing `from`.
1166
+ if (typeof from === 'string')
1167
+ return checkFrom(component, name, from, ofName, state);
1168
+ // a lens or an invalid value: the core's (SYG401 / SYG412)
1161
1169
  if (from !== undefined)
1162
1170
  return;
1163
- if (!Array.isArray(state) && !(state && Array.isArray(state.value))) {
1164
- problem = `has no 'from' prop, so it uses ${name}'s whole state, which is ${describe$1(state)}, not an array`;
1165
- }
1166
- if (!problem || !once(`SYG401:${name}:undefined`))
1171
+ if (Array.isArray(state) || (state && Array.isArray(state.value)))
1172
+ return;
1173
+ if (!once(`SYG401:${name}:undefined`))
1167
1174
  return;
1168
1175
  reportSafely('SYG401', {
1169
1176
  component,
1170
- message: `<Collection of={${ofName}}> ${problem}, so it renders no items`,
1171
- fix,
1177
+ message: `<Collection of={${ofName}}> has no 'from' prop, so it uses ${name}'s whole state, which is ${describe$1(state)}, not an array, so it renders no items`,
1178
+ fix: `Pass the name of an array field in ${name}'s state, e.g. <Collection of={${ofName}} from="items" />, and initialize that field to [] in initialState`,
1172
1179
  data: { from: 'undefined', of: ofName },
1173
1180
  });
1174
1181
  }
1182
+ /** `from` a key or a path ('board.cards', 'boards[0].cards'), as the core reads it (cell.ts pathOf) */
1183
+ function checkFrom(component, name, from, ofName, state) {
1184
+ const ks = from.replace(/\[(\d+)\]/g, '.$1').split('.');
1185
+ const calc = component._calculatedFieldNames;
1186
+ if (calc?.has?.(ks[0]))
1187
+ return;
1188
+ const at = ks.slice(0, -1).reduce((o, k) => (o == null ? o : o[k]), state), last = ks[ks.length - 1];
1189
+ if (!isPlainObject(at) && !Array.isArray(at))
1190
+ return; // a level still missing: renders once it appears (D178)
1191
+ let message;
1192
+ if (!(last in at)) {
1193
+ const arrays = Object.keys(at).filter(k => Array.isArray(at[k]));
1194
+ message = `Collection from="${from}" is not in state${arrays.length ? ` (array fields: '${arrays.join("', '")}')` : ''}; it renders nothing until it exists`;
1195
+ }
1196
+ else if (!Array.isArray(at[last]))
1197
+ message = `Collection 'from' field '${from}' is not an array; it renders nothing`;
1198
+ else
1199
+ return;
1200
+ if (!once(`SYG401:${name}:${from}`))
1201
+ return;
1202
+ reportSafely('SYG401', { component, message, fix: 'Set it to an array in initialState', data: { from, of: ofName } });
1203
+ }
1175
1204
  const WRAPPED = '__sygnalDiagWrapped';
1176
1205
  const collectionsCheck = {
1177
1206
  id: 'collections',
@@ -1244,7 +1273,7 @@ onReset(() => {
1244
1273
  });
1245
1274
  /** the control that rendered a vnode (only while the dev entry is installed) */
1246
1275
  const controlOfVnode = (vnode) => vnode && typeof vnode == 'object' ? vnodeControls.get(vnode) : undefined;
1247
- const STATICS$1 = ['intent', 'model', 'initialState'];
1276
+ const STATICS$2 = ['intent', 'model', 'initialState'];
1248
1277
  const describe = (v) => v === undefined || v === null ? String(v)
1249
1278
  : Array.isArray(v) ? `an array of ${v.length}`
1250
1279
  : typeof v != 'object' ? `a ${typeof v} (${JSON.stringify(v)})`
@@ -1258,7 +1287,7 @@ function onControl(control, vnode) {
1258
1287
  vnodeControls.set(vnode, control);
1259
1288
  if (!staticsSeen.has(control)) {
1260
1289
  staticsSeen.add(control);
1261
- const given = STATICS$1.filter(s => control[s] != null);
1290
+ const given = STATICS$2.filter(s => control[s] != null);
1262
1291
  if (given.length) {
1263
1292
  devReport('SYG125', {
1264
1293
  message: `The control ${key} has ${given.map(s => '.' + s).join(', ')}, but controls are elements, not components: ${given.length > 1 ? 'these statics are' : 'it is'} never used`,
@@ -3455,6 +3484,9 @@ const slim = (d) => {
3455
3484
  out.fix = d.fix;
3456
3485
  if (d.docsUrl)
3457
3486
  out.docsUrl = d.docsUrl;
3487
+ // PLAN-10 P-1: when it was reported (runtime diagnostics are reported once per process)
3488
+ if (typeof d.timestamp === 'number')
3489
+ out.timestamp = d.timestamp;
3458
3490
  if (d.data !== undefined) {
3459
3491
  // keep it JSON-safe (data can hold errors, vnodes, ...)
3460
3492
  try {
@@ -3558,12 +3590,61 @@ function childrenOf(r, all) {
3558
3590
  const key = `${c.name}\u0000${via[c.kind]}`;
3559
3591
  const entry = out.get(key);
3560
3592
  if (entry)
3561
- entry.count = (entry.count || 1) + 1;
3593
+ entry.count = entry.ids.push(c.id);
3562
3594
  else
3563
- out.set(key, { name: c.name, via: via[c.kind], count: 1 });
3595
+ out.set(key, { name: c.name, via: via[c.kind], count: 1, ids: [c.id] });
3564
3596
  }
3565
3597
  return [...out.values()];
3566
3598
  }
3599
+ /** the statics sygnal-check reads (its STATIC_PROPS): a component function with none is view-only */
3600
+ const STATICS$1 = ['intent', 'model', 'initialState', 'isolatedState', 'context', 'calculated', 'connections', 'resources', 'route', 'head', 'uses', 'timers', 'persist', 'viewTransitions', 'browser', 'agent'];
3601
+ /** PLAN-10 D522: no statics and nothing the core added (renderComponent's test intent, a behavior's model) */
3602
+ function isViewOnly(c) {
3603
+ const def = c && c.__next && c.__next.def;
3604
+ if (!def || def.intent || (def.handlers && def.handlers.size) || def.initialState !== undefined)
3605
+ return false;
3606
+ const fn = def.view;
3607
+ return !!fn && STATICS$1.every(k => fn[k] === undefined);
3608
+ }
3609
+ /** PLAN-10 P-2 (D544): the static key sygnal/vite's dev transform stamped on the component function */
3610
+ function stampedKey(c) {
3611
+ const fn = c && c.__next && c.__next.def && c.__next.def.view;
3612
+ const k = fn && fn.__sygnalKey;
3613
+ return typeof k === 'string' ? k : undefined;
3614
+ }
3615
+ /** PLAN-10 D530: the context keys the instance's latest render read (InstanceView.reads); every key when it read them all */
3616
+ function contextReadsOf(c) {
3617
+ const v = c && c.__next;
3618
+ const reads = v && v.reads;
3619
+ if (!reads)
3620
+ return [];
3621
+ const out = [...reads].filter(k => k !== '\u0000all');
3622
+ if (reads.has('\u0000all')) {
3623
+ try {
3624
+ for (const k of Object.keys(v.context || {}))
3625
+ if (!out.includes(k))
3626
+ out.push(k);
3627
+ }
3628
+ catch (_) { /* no context */ }
3629
+ }
3630
+ return out;
3631
+ }
3632
+ /** PLAN-10 D522: the app (run()) of each instance: the index of its root among the live roots, oldest first */
3633
+ function appsOf(all) {
3634
+ const byId = new Map(all.map(r => [r.id, r]));
3635
+ const roots = all.filter(r => r.parentId === null).sort((a, b) => Number(a.id) - Number(b.id));
3636
+ const index = new Map(roots.map((r, i) => [r, i]));
3637
+ const out = new Map();
3638
+ for (const r of all) {
3639
+ let top = r;
3640
+ for (let n = 0; top && top.parentId !== null && n < 10000; n++)
3641
+ top = byId.get(top.parentId);
3642
+ const i = top && index.get(top);
3643
+ if (i !== undefined)
3644
+ out.set(r, i);
3645
+ }
3646
+ return out;
3647
+ }
3567
3648
  /** The app graph of the live component instances (see ./public.d.ts). */
3568
3649
  function inspect(options = {}) {
3569
3650
  // report SYG105 (info) for EVENTS types selected but never emitted so far
@@ -3588,21 +3669,28 @@ function inspect(options = {}) {
3588
3669
  const ids = options.ids && new Set(options.ids.map(String));
3589
3670
  const recs = ids ? all.filter(r => ids.has(r.id)) : all;
3590
3671
  const names = new Set(recs.map(r => r.name));
3672
+ const apps = appsOf(all);
3591
3673
  const components = recs.map(r => {
3592
3674
  const c = r.instance;
3593
3675
  const calculated = c && c.calculated && typeof c.calculated === 'object' ? Object.keys(c.calculated) : [];
3594
3676
  const selectors = selectorsOf(r, options, diags);
3595
3677
  const controls = controlsOf(r, selectors);
3678
+ const key = stampedKey(c);
3596
3679
  return {
3597
3680
  name: r.name,
3598
3681
  id: r.id,
3682
+ ...(key ? { key } : {}),
3599
3683
  parentId: r.parentId,
3600
3684
  kind: r.kind,
3685
+ ...(isViewOnly(c) ? { viewOnly: true } : {}),
3686
+ ...(r.parentId === null ? { entry: true } : {}),
3687
+ ...(apps.has(r) ? { app: apps.get(r) } : {}),
3688
+ ...(c && c.__next && c.__next.shown === false ? { shown: false } : {}),
3601
3689
  actions: actionsOf(r),
3602
3690
  stateKeys: stateKeysOf(c, calculated),
3603
3691
  calculated,
3604
3692
  contextProvides: c && isPlainObject(c.context) ? Object.keys(c.context) : [],
3605
- contextConsumes: null,
3693
+ contextConsumes: contextReadsOf(c),
3606
3694
  eventsEmitted: [...r.eventsEmitted],
3607
3695
  eventsSelected: [...r.eventsSelected],
3608
3696
  children: childrenOf(r, all),
@@ -3614,14 +3702,21 @@ function inspect(options = {}) {
3614
3702
  });
3615
3703
  const cache = cacheOf(recs) || {};
3616
3704
  const events = {};
3617
- const ev = (type) => events[type] || (events[type] = { emitters: [], selectors: [] });
3705
+ // names once each (as before); PLAN-10 D522: every instance's id in emitterIds / selectorIds
3706
+ const ev = (type) => events[type] || (events[type] = { emitters: [], selectors: [], emitterIds: [], selectorIds: [] });
3618
3707
  const add = (list, name) => { if (!list.includes(name))
3619
3708
  list.push(name); };
3620
3709
  for (const c of components) {
3621
- for (const t of c.eventsEmitted)
3622
- add(ev(t).emitters, c.name);
3623
- for (const t of c.eventsSelected)
3624
- add(ev(t).selectors, c.name);
3710
+ for (const t of c.eventsEmitted) {
3711
+ const e = ev(t);
3712
+ add(e.emitters, c.name);
3713
+ e.emitterIds.push(c.id);
3714
+ }
3715
+ for (const t of c.eventsSelected) {
3716
+ const e = ev(t);
3717
+ add(e.selectors, c.name);
3718
+ e.selectorIds.push(c.id);
3719
+ }
3625
3720
  }
3626
3721
  // emitted on the bus by something that isn't a listed component (e.g. a driver, a pruned component)
3627
3722
  if (!ids)
@@ -3736,6 +3831,22 @@ function removed(component, name, anchor, detail) {
3736
3831
  data: { form: anchor, guide: `${MIGRATE}#${anchor}`, ...(detail ? { detail } : {}) },
3737
3832
  });
3738
3833
  }
3834
+ /**
3835
+ * SYG428 (6.5.0): `state="a.b"` / `from="a.b"` is a path, read as `state.a.b`. Before 6.5.0 it was
3836
+ * one key named "a.b"; when the owner's state still has such a key, the designator now misses it.
3837
+ */
3838
+ function dottedKey(f, what, designator, ownerState) {
3839
+ if (typeof designator != 'string' || !/[.[]/.test(designator) || !isPlainObject(ownerState))
3840
+ return;
3841
+ if (!Object.prototype.hasOwnProperty.call(ownerState, designator) || !once(`SYG428:${f.name}:${designator}`))
3842
+ return;
3843
+ devReport('SYG428', {
3844
+ component: f.name,
3845
+ message: `${what}="${designator}" is a path (since 6.5.0), read as ${designator.replace(/\[(\d+)\]/g, '.$1').split('.').map((k) => `[${JSON.stringify(k)}]`).join('')}, but the state has a key named "${designator}"`,
3846
+ fix: `Rename the key so it has no dot (and point ${what} at it), or use a lens: ${what}={{ get: (s) => s['${designator}'], set: (s, v) => ({ ...s, '${designator}': v }) }}`,
3847
+ data: { designator, what },
3848
+ });
3849
+ }
3739
3850
  /** SYG427: CHILD.select(x) with no component (an import of a name the module doesn't export) */
3740
3851
  function notComponent(f, x) {
3741
3852
  if (!once(`SYG427:${f.name}`))
@@ -4102,12 +4213,18 @@ function nextHooks(_api) {
4102
4213
  // G-527: one chain of SYG149 checks over the latest patches (G-501: it stops once every instance
4103
4214
  // that renders a named Collection is disposed, the app's dispose included)
4104
4215
  const vtCheck = vtChecker(() => vtp.size && on());
4216
+ // SYG428: the live instances by id, to read a child's parent state
4217
+ const live = new Map();
4105
4218
  return {
4106
4219
  onCreate(iv) {
4220
+ live.set(iv.id, iv);
4107
4221
  if (!on())
4108
4222
  return;
4109
4223
  log.onCreate(iv);
4110
4224
  const f = fac(iv);
4225
+ const parent = live.get(iv.parentId);
4226
+ if (parent)
4227
+ dottedKey(f, `<${f.name} state`, iv.props?.state, parent.state);
4111
4228
  removedStatics(f);
4112
4229
  if (!iv.def.intent)
4113
4230
  wire(iv, []);
@@ -4166,6 +4283,7 @@ function nextHooks(_api) {
4166
4283
  },
4167
4284
  onDispose(iv) {
4168
4285
  vtp.delete(iv);
4286
+ live.delete(iv.id);
4169
4287
  H.onDispose(fac(iv));
4170
4288
  },
4171
4289
  onAction(iv, a) { log.onAction(iv, a); },
@@ -4213,6 +4331,7 @@ function nextHooks(_api) {
4213
4331
  checkCollection(f, { data: { props } });
4214
4332
  }
4215
4333
  catch (_) { /* ignore */ }
4334
+ dottedKey(f, `<${sel == 'collection' ? 'Collection' : 'VirtualCollection'} from`, props.from, owner.state);
4216
4335
  // G-419: SYG148 for a prefix that isn't an identifier; a valid one is checked for
4217
4336
  // duplicate names after each patch (SYG149). G-460: a falsy one names nothing (as the host)
4218
4337
  const p = props.viewTransitionName;