sygnal 6.3.0 → 6.5.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 (42) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/astro/index.cjs.js +69 -3
  3. package/dist/astro/index.cjs.js.map +1 -1
  4. package/dist/astro/index.mjs +69 -3
  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/diagnostics.cjs.js +105 -19
  11. package/dist/diagnostics.cjs.js.map +1 -1
  12. package/dist/diagnostics.esm.js +105 -19
  13. package/dist/diagnostics.esm.js.map +1 -1
  14. package/dist/guide/mcp-apps.md +59 -4
  15. package/dist/index.cjs.js +54 -25
  16. package/dist/index.cjs.js.map +1 -1
  17. package/dist/index.d.ts +27 -6
  18. package/dist/index.esm.js +54 -25
  19. package/dist/index.esm.js.map +1 -1
  20. package/dist/sygnal.min.js +1 -1
  21. package/dist/sygnal.min.js.map +1 -1
  22. package/dist/vike/config/package.json +1 -1
  23. package/dist/vite/plugin.cjs.js +69 -3
  24. package/dist/vite/plugin.cjs.js.map +1 -1
  25. package/dist/vite/plugin.mjs +69 -3
  26. package/dist/vite/plugin.mjs.map +1 -1
  27. package/llms.txt +4 -4
  28. package/package.json +1 -1
  29. package/src/core/cell.ts +25 -7
  30. package/src/core/hosts/collection.ts +6 -11
  31. package/src/core/hosts/switchable.ts +2 -2
  32. package/src/core/instance.ts +2 -2
  33. package/src/extra/diagnostics/checks/collections.ts +31 -25
  34. package/src/extra/diagnostics/checks/dom.ts +10 -2
  35. package/src/extra/diagnostics/checks/next.ts +39 -1
  36. package/src/extra/diagnostics/checks/persist.ts +4 -2
  37. package/src/extra/diagnostics/checks/wiring.ts +14 -6
  38. package/src/extra/diagnostics/codes.ts +9 -0
  39. package/src/extra/pwa.ts +3 -1
  40. package/src/extra/reducers.ts +10 -4
  41. package/src/index.d.ts +26 -5
  42. package/src/vite/plugin.ts +63 -2
@@ -269,6 +269,11 @@ const CODE_TITLES = {
269
269
  SYG425: 'isolatedState slice lacks initialState keys',
270
270
  // PLAN-9 (D471): checks/next.ts; dev entry and sygnal-check
271
271
  SYG426: 'Deprecated lowercase marker tag',
272
+ // checks/next.ts and sygnal-check: a component reference that is not a function (an import of
273
+ // a name the module doesn't export is undefined under Vitest)
274
+ SYG427: 'CHILD.select() of something that is not a component',
275
+ // 6.5.0 (checks/next.ts): state="a.b" / from="a.b" is a path; the parent state has a literal "a.b" key
276
+ SYG428: 'A state path that names a key containing a dot',
272
277
  // PLAN-9 (D470): checks/next.ts; dev entry
273
278
  SYG453: 'Two children with the same identity share one instance',
274
279
  SYG601: "Retired in 6.0: Missing or invalid sources",
@@ -475,6 +480,10 @@ const DEV_CODE_SEVERITY = {
475
480
  SYG425: 'warn',
476
481
  // PLAN-9 (checks/next.ts): D471 a lowercase <collection> / <switchable> tag (deprecated in 6.3.0)
477
482
  SYG426: 'warn',
483
+ // checks/next.ts: CHILD.select(undefined), usually a missing export
484
+ SYG427: 'error',
485
+ // checks/next.ts: a path designator that 6.4 read as one key with a dot
486
+ SYG428: 'warn',
478
487
  // PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
479
488
  SYG453: 'warn',
480
489
  SYG612: 'error',
@@ -791,11 +800,20 @@ const underTest = (c) => {
791
800
  const dom = c?.sources?.[c.DOMSourceName || 'DOM'];
792
801
  return !!dom && typeof dom == 'object' && '_hub' in dom;
793
802
  };
794
- // G-620: an LLM / chat sink gets the chat driver's fix, not makeFetchDriver's
795
- const driverFix = (sink) => `Pass a driver named ${sink} to run(): ` + (/^(LLM|CHAT|AI|ASSISTANT)$/.test(sink)
796
- ? `run(App, { ${sink}: makeChatDriver({ transport: openResponses({ baseURL: 'http://localhost:11434/v1', model: 'llama3.2' }) }) }) (from 'sygnal/ai'; uiMessageStream('/api/chat') for an AI SDK server)`
797
- : `run(App, { ${sink}: makeFetchDriver() }) for HTTP, or driverFromAsync(fn) / your own driver`) +
798
- `. Check the spelling against the drivers you pass`;
803
+ // G-620: the usual driver for a known sink name (an LLM / chat sink gets the chat driver's fix,
804
+ // SW the service worker's), the generic text for any other name
805
+ const DRIVER_FOR = [
806
+ [/^(LLM|CHAT|AI|ASSISTANT)$/, (s) => `run(App, { ${s}: makeChatDriver({ transport: openResponses({ baseURL: 'http://localhost:11434/v1', model: 'llama3.2' }) }) }) (from 'sygnal/ai'; uiMessageStream('/api/chat') for an AI SDK server)`],
807
+ [/^(SW|SERVICE_WORKER)$/, (s) => `run(App, { ${s}: makeServiceWorkerDriver('/sw.js', { enabled: import.meta.env.PROD }) }) (enabled: false keeps the driver, with no service worker, on a dev server)`],
808
+ [/^(HTTP|API|FETCH)$/, (s) => `run(App, { ${s}: makeFetchDriver() })`],
809
+ [/^(WS|SOCKET)$/, (s) => `run(App, { ${s}: makeSocketDriver() })`],
810
+ ];
811
+ const driverFix = (sink) => {
812
+ const known = DRIVER_FOR.find(([re]) => re.test(sink));
813
+ return `Pass a driver named ${sink} to run(): ` +
814
+ (known ? known[1](sink) : `run(App, { ${sink}: makeFetchDriver() }) for HTTP, or driverFromAsync(fn) / your own driver`) +
815
+ `. Check the spelling against the drivers you pass`;
816
+ };
799
817
  const wiringCheck = {
800
818
  id: 'wiring',
801
819
  onIntent(component, actionNames) {
@@ -1139,25 +1157,44 @@ function checkCollection(component, el) {
1139
1157
  state = component.addCalculated(state);
1140
1158
  }
1141
1159
  catch (_) { }
1142
- let problem;
1143
- 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`;
1144
- // Every defined `from` (string field, lens object, or an invalid value) is reported by
1145
- // the core itself (SYG401/SYG412, retrofitted in 1E) in every diagnostics mode (D22), so
1146
- // this check only covers a missing `from`.
1160
+ if (typeof from === 'string')
1161
+ return checkFrom(component, name, from, ofName, state);
1162
+ // a lens or an invalid value: the core's (SYG401 / SYG412)
1147
1163
  if (from !== undefined)
1148
1164
  return;
1149
- if (!Array.isArray(state) && !(state && Array.isArray(state.value))) {
1150
- problem = `has no 'from' prop, so it uses ${name}'s whole state, which is ${describe$1(state)}, not an array`;
1151
- }
1152
- if (!problem || !once(`SYG401:${name}:undefined`))
1165
+ if (Array.isArray(state) || (state && Array.isArray(state.value)))
1166
+ return;
1167
+ if (!once(`SYG401:${name}:undefined`))
1153
1168
  return;
1154
1169
  reportSafely('SYG401', {
1155
1170
  component,
1156
- message: `<Collection of={${ofName}}> ${problem}, so it renders no items`,
1157
- fix,
1171
+ 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`,
1172
+ 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
1173
  data: { from: 'undefined', of: ofName },
1159
1174
  });
1160
1175
  }
1176
+ /** `from` a key or a path ('board.cards', 'boards[0].cards'), as the core reads it (cell.ts pathOf) */
1177
+ function checkFrom(component, name, from, ofName, state) {
1178
+ const ks = from.replace(/\[(\d+)\]/g, '.$1').split('.');
1179
+ const calc = component._calculatedFieldNames;
1180
+ if (calc?.has?.(ks[0]))
1181
+ return;
1182
+ const at = ks.slice(0, -1).reduce((o, k) => (o == null ? o : o[k]), state), last = ks[ks.length - 1];
1183
+ if (!isPlainObject(at) && !Array.isArray(at))
1184
+ return; // a level still missing: renders once it appears (D178)
1185
+ let message;
1186
+ if (!(last in at)) {
1187
+ const arrays = Object.keys(at).filter(k => Array.isArray(at[k]));
1188
+ message = `Collection from="${from}" is not in state${arrays.length ? ` (array fields: '${arrays.join("', '")}')` : ''}; it renders nothing until it exists`;
1189
+ }
1190
+ else if (!Array.isArray(at[last]))
1191
+ message = `Collection 'from' field '${from}' is not an array; it renders nothing`;
1192
+ else
1193
+ return;
1194
+ if (!once(`SYG401:${name}:${from}`))
1195
+ return;
1196
+ reportSafely('SYG401', { component, message, fix: 'Set it to an array in initialState', data: { from, of: ofName } });
1197
+ }
1161
1198
  const WRAPPED = '__sygnalDiagWrapped';
1162
1199
  const collectionsCheck = {
1163
1200
  id: 'collections',
@@ -1344,6 +1381,10 @@ onReset(() => {
1344
1381
  });
1345
1382
  const unref = (t) => { if (t && typeof t.unref === 'function')
1346
1383
  t.unref(); return t; };
1384
+ /**
1385
+ * The view's source, plus the words sygnal/vite registered for the component (its module's and
1386
+ * its relative imports' string literals: markup a view helper draws), when it did
1387
+ */
1347
1388
  function viewSource(component) {
1348
1389
  const view = component && component.view;
1349
1390
  if (typeof view !== 'function')
@@ -1356,6 +1397,9 @@ function viewSource(component) {
1356
1397
  catch (_) {
1357
1398
  src = '';
1358
1399
  }
1400
+ const registered = globalThis.__SYGNAL_VIEW_TOKENS__?.get?.(view);
1401
+ if (typeof registered == 'string')
1402
+ src += '\n' + registered;
1359
1403
  viewTokens.set(view, src);
1360
1404
  }
1361
1405
  return src;
@@ -3000,7 +3044,8 @@ const persistCheck = {
3000
3044
  const o = p.options, init = view.initialState;
3001
3045
  if (!o || !init || typeof init != 'object')
3002
3046
  return;
3003
- const keys = Object.keys(init);
3047
+ // a behavior's slice (`uses = { chat: chat() }`) is a state key too
3048
+ const keys = [...Object.keys(init), ...Object.keys(view.uses || {})];
3004
3049
  for (const opt of ['pick', 'omit']) {
3005
3050
  for (const k of Array.isArray(o[opt]) ? o[opt] : []) {
3006
3051
  if (keys.includes(k) || !once(`SYG223:${name}:${opt}:${k}`))
@@ -3714,6 +3759,34 @@ function removed(component, name, anchor, detail) {
3714
3759
  data: { form: anchor, guide: `${MIGRATE}#${anchor}`, ...(detail ? { detail } : {}) },
3715
3760
  });
3716
3761
  }
3762
+ /**
3763
+ * SYG428 (6.5.0): `state="a.b"` / `from="a.b"` is a path, read as `state.a.b`. Before 6.5.0 it was
3764
+ * one key named "a.b"; when the owner's state still has such a key, the designator now misses it.
3765
+ */
3766
+ function dottedKey(f, what, designator, ownerState) {
3767
+ if (typeof designator != 'string' || !/[.[]/.test(designator) || !isPlainObject(ownerState))
3768
+ return;
3769
+ if (!Object.prototype.hasOwnProperty.call(ownerState, designator) || !once(`SYG428:${f.name}:${designator}`))
3770
+ return;
3771
+ devReport('SYG428', {
3772
+ component: f.name,
3773
+ 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}"`,
3774
+ 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 }) }}`,
3775
+ data: { designator, what },
3776
+ });
3777
+ }
3778
+ /** SYG427: CHILD.select(x) with no component (an import of a name the module doesn't export) */
3779
+ function notComponent(f, x) {
3780
+ if (!once(`SYG427:${f.name}`))
3781
+ return;
3782
+ const got = x === null ? 'null' : typeof x == 'object' ? 'an object' : String(x);
3783
+ devReport('SYG427', {
3784
+ component: f.name,
3785
+ message: `${f.name}'s intent calls CHILD.select(${got}): that is not a component, so it never emits`,
3786
+ fix: "Pass the child component itself, and check its import: a name its module doesn't export is undefined (Vitest runs on; the browser fails to link the module). Export it: export function Child() { … }",
3787
+ data: { got },
3788
+ });
3789
+ }
3717
3790
  /** the removed forms a definition shows (statics, view arity, model keys), once per function */
3718
3791
  let seenDefs = new WeakSet();
3719
3792
  // G-332: resetChecks() forgets them (as the once() dedupe)
@@ -4068,12 +4141,18 @@ function nextHooks(_api) {
4068
4141
  // G-527: one chain of SYG149 checks over the latest patches (G-501: it stops once every instance
4069
4142
  // that renders a named Collection is disposed, the app's dispose included)
4070
4143
  const vtCheck = vtChecker(() => vtp.size && on());
4144
+ // SYG428: the live instances by id, to read a child's parent state
4145
+ const live = new Map();
4071
4146
  return {
4072
4147
  onCreate(iv) {
4148
+ live.set(iv.id, iv);
4073
4149
  if (!on())
4074
4150
  return;
4075
4151
  log.onCreate(iv);
4076
4152
  const f = fac(iv);
4153
+ const parent = live.get(iv.parentId);
4154
+ if (parent)
4155
+ dottedKey(f, `<${f.name} state`, iv.props?.state, parent.state);
4077
4156
  removedStatics(f);
4078
4157
  if (!iv.def.intent)
4079
4158
  wire(iv, []);
@@ -4097,8 +4176,13 @@ function nextHooks(_api) {
4097
4176
  const v = t[k];
4098
4177
  if (k !== 'CHILD' || !v || typeof v.select != 'function')
4099
4178
  return v;
4100
- return { ...v, select: (x, ...r) => { if (typeof x == 'string')
4101
- removed(f, f.name, 'child-select-name', `CHILD.select('${x}')`); return v.select(x, ...r); } };
4179
+ return { ...v, select: (x, ...r) => {
4180
+ if (typeof x == 'string')
4181
+ removed(f, f.name, 'child-select-name', `CHILD.select('${x}')`);
4182
+ else if (typeof x != 'function')
4183
+ notComponent(f, x);
4184
+ return v.select(x, ...r);
4185
+ } };
4102
4186
  },
4103
4187
  });
4104
4188
  },
@@ -4127,6 +4211,7 @@ function nextHooks(_api) {
4127
4211
  },
4128
4212
  onDispose(iv) {
4129
4213
  vtp.delete(iv);
4214
+ live.delete(iv.id);
4130
4215
  H.onDispose(fac(iv));
4131
4216
  },
4132
4217
  onAction(iv, a) { log.onAction(iv, a); },
@@ -4174,6 +4259,7 @@ function nextHooks(_api) {
4174
4259
  checkCollection(f, { data: { props } });
4175
4260
  }
4176
4261
  catch (_) { /* ignore */ }
4262
+ dottedKey(f, `<${sel == 'collection' ? 'Collection' : 'VirtualCollection'} from`, props.from, owner.state);
4177
4263
  // G-419: SYG148 for a prefix that isn't an identifier; a valid one is checked for
4178
4264
  // duplicate names after each patch (SYG149). G-460: a falsy one names nothing (as the host)
4179
4265
  const p = props.viewTransitionName;