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
@@ -249,6 +249,11 @@ const CODE_TITLES = {
249
249
  SYG425: 'isolatedState slice lacks initialState keys',
250
250
  // PLAN-9 (D471): checks/next.ts; dev entry and sygnal-check
251
251
  SYG426: 'Deprecated lowercase marker tag',
252
+ // checks/next.ts and sygnal-check: a component reference that is not a function (an import of
253
+ // a name the module doesn't export is undefined under Vitest)
254
+ SYG427: 'CHILD.select() of something that is not a component',
255
+ // 6.5.0 (checks/next.ts): state="a.b" / from="a.b" is a path; the parent state has a literal "a.b" key
256
+ SYG428: 'A state path that names a key containing a dot',
252
257
  // PLAN-9 (D470): checks/next.ts; dev entry
253
258
  SYG453: 'Two children with the same identity share one instance',
254
259
  SYG601: "Retired in 6.0: Missing or invalid sources",
@@ -455,6 +460,10 @@ const DEV_CODE_SEVERITY = {
455
460
  SYG425: 'warn',
456
461
  // PLAN-9 (checks/next.ts): D471 a lowercase <collection> / <switchable> tag (deprecated in 6.3.0)
457
462
  SYG426: 'warn',
463
+ // checks/next.ts: CHILD.select(undefined), usually a missing export
464
+ SYG427: 'error',
465
+ // checks/next.ts: a path designator that 6.4 read as one key with a dot
466
+ SYG428: 'warn',
458
467
  // PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
459
468
  SYG453: 'warn',
460
469
  SYG612: 'error',
@@ -771,11 +780,20 @@ const underTest = (c) => {
771
780
  const dom = c?.sources?.[c.DOMSourceName || 'DOM'];
772
781
  return !!dom && typeof dom == 'object' && '_hub' in dom;
773
782
  };
774
- // G-620: an LLM / chat sink gets the chat driver's fix, not makeFetchDriver's
775
- const driverFix = (sink) => `Pass a driver named ${sink} to run(): ` + (/^(LLM|CHAT|AI|ASSISTANT)$/.test(sink)
776
- ? `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)`
777
- : `run(App, { ${sink}: makeFetchDriver() }) for HTTP, or driverFromAsync(fn) / your own driver`) +
778
- `. Check the spelling against the drivers you pass`;
783
+ // G-620: the usual driver for a known sink name (an LLM / chat sink gets the chat driver's fix,
784
+ // SW the service worker's), the generic text for any other name
785
+ const DRIVER_FOR = [
786
+ [/^(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)`],
787
+ [/^(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)`],
788
+ [/^(HTTP|API|FETCH)$/, (s) => `run(App, { ${s}: makeFetchDriver() })`],
789
+ [/^(WS|SOCKET)$/, (s) => `run(App, { ${s}: makeSocketDriver() })`],
790
+ ];
791
+ const driverFix = (sink) => {
792
+ const known = DRIVER_FOR.find(([re]) => re.test(sink));
793
+ return `Pass a driver named ${sink} to run(): ` +
794
+ (known ? known[1](sink) : `run(App, { ${sink}: makeFetchDriver() }) for HTTP, or driverFromAsync(fn) / your own driver`) +
795
+ `. Check the spelling against the drivers you pass`;
796
+ };
779
797
  const wiringCheck = {
780
798
  id: 'wiring',
781
799
  onIntent(component, actionNames) {
@@ -1119,25 +1137,44 @@ function checkCollection(component, el) {
1119
1137
  state = component.addCalculated(state);
1120
1138
  }
1121
1139
  catch (_) { }
1122
- let problem;
1123
- 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`;
1124
- // Every defined `from` (string field, lens object, or an invalid value) is reported by
1125
- // the core itself (SYG401/SYG412, retrofitted in 1E) in every diagnostics mode (D22), so
1126
- // this check only covers a missing `from`.
1140
+ if (typeof from === 'string')
1141
+ return checkFrom(component, name, from, ofName, state);
1142
+ // a lens or an invalid value: the core's (SYG401 / SYG412)
1127
1143
  if (from !== undefined)
1128
1144
  return;
1129
- if (!Array.isArray(state) && !(state && Array.isArray(state.value))) {
1130
- problem = `has no 'from' prop, so it uses ${name}'s whole state, which is ${describe$1(state)}, not an array`;
1131
- }
1132
- if (!problem || !once(`SYG401:${name}:undefined`))
1145
+ if (Array.isArray(state) || (state && Array.isArray(state.value)))
1146
+ return;
1147
+ if (!once(`SYG401:${name}:undefined`))
1133
1148
  return;
1134
1149
  reportSafely('SYG401', {
1135
1150
  component,
1136
- message: `<Collection of={${ofName}}> ${problem}, so it renders no items`,
1137
- fix,
1151
+ 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`,
1152
+ 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`,
1138
1153
  data: { from: 'undefined', of: ofName },
1139
1154
  });
1140
1155
  }
1156
+ /** `from` a key or a path ('board.cards', 'boards[0].cards'), as the core reads it (cell.ts pathOf) */
1157
+ function checkFrom(component, name, from, ofName, state) {
1158
+ const ks = from.replace(/\[(\d+)\]/g, '.$1').split('.');
1159
+ const calc = component._calculatedFieldNames;
1160
+ if (calc?.has?.(ks[0]))
1161
+ return;
1162
+ const at = ks.slice(0, -1).reduce((o, k) => (o == null ? o : o[k]), state), last = ks[ks.length - 1];
1163
+ if (!isPlainObject(at) && !Array.isArray(at))
1164
+ return; // a level still missing: renders once it appears (D178)
1165
+ let message;
1166
+ if (!(last in at)) {
1167
+ const arrays = Object.keys(at).filter(k => Array.isArray(at[k]));
1168
+ message = `Collection from="${from}" is not in state${arrays.length ? ` (array fields: '${arrays.join("', '")}')` : ''}; it renders nothing until it exists`;
1169
+ }
1170
+ else if (!Array.isArray(at[last]))
1171
+ message = `Collection 'from' field '${from}' is not an array; it renders nothing`;
1172
+ else
1173
+ return;
1174
+ if (!once(`SYG401:${name}:${from}`))
1175
+ return;
1176
+ reportSafely('SYG401', { component, message, fix: 'Set it to an array in initialState', data: { from, of: ofName } });
1177
+ }
1141
1178
  const WRAPPED = '__sygnalDiagWrapped';
1142
1179
  const collectionsCheck = {
1143
1180
  id: 'collections',
@@ -1324,6 +1361,10 @@ onReset(() => {
1324
1361
  });
1325
1362
  const unref = (t) => { if (t && typeof t.unref === 'function')
1326
1363
  t.unref(); return t; };
1364
+ /**
1365
+ * The view's source, plus the words sygnal/vite registered for the component (its module's and
1366
+ * its relative imports' string literals: markup a view helper draws), when it did
1367
+ */
1327
1368
  function viewSource(component) {
1328
1369
  const view = component && component.view;
1329
1370
  if (typeof view !== 'function')
@@ -1336,6 +1377,9 @@ function viewSource(component) {
1336
1377
  catch (_) {
1337
1378
  src = '';
1338
1379
  }
1380
+ const registered = globalThis.__SYGNAL_VIEW_TOKENS__?.get?.(view);
1381
+ if (typeof registered == 'string')
1382
+ src += '\n' + registered;
1339
1383
  viewTokens.set(view, src);
1340
1384
  }
1341
1385
  return src;
@@ -2980,7 +3024,8 @@ const persistCheck = {
2980
3024
  const o = p.options, init = view.initialState;
2981
3025
  if (!o || !init || typeof init != 'object')
2982
3026
  return;
2983
- const keys = Object.keys(init);
3027
+ // a behavior's slice (`uses = { chat: chat() }`) is a state key too
3028
+ const keys = [...Object.keys(init), ...Object.keys(view.uses || {})];
2984
3029
  for (const opt of ['pick', 'omit']) {
2985
3030
  for (const k of Array.isArray(o[opt]) ? o[opt] : []) {
2986
3031
  if (keys.includes(k) || !once(`SYG223:${name}:${opt}:${k}`))
@@ -3694,6 +3739,34 @@ function removed(component, name, anchor, detail) {
3694
3739
  data: { form: anchor, guide: `${MIGRATE}#${anchor}`, ...(detail ? { detail } : {}) },
3695
3740
  });
3696
3741
  }
3742
+ /**
3743
+ * SYG428 (6.5.0): `state="a.b"` / `from="a.b"` is a path, read as `state.a.b`. Before 6.5.0 it was
3744
+ * one key named "a.b"; when the owner's state still has such a key, the designator now misses it.
3745
+ */
3746
+ function dottedKey(f, what, designator, ownerState) {
3747
+ if (typeof designator != 'string' || !/[.[]/.test(designator) || !isPlainObject(ownerState))
3748
+ return;
3749
+ if (!Object.prototype.hasOwnProperty.call(ownerState, designator) || !once(`SYG428:${f.name}:${designator}`))
3750
+ return;
3751
+ devReport('SYG428', {
3752
+ component: f.name,
3753
+ 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}"`,
3754
+ 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 }) }}`,
3755
+ data: { designator, what },
3756
+ });
3757
+ }
3758
+ /** SYG427: CHILD.select(x) with no component (an import of a name the module doesn't export) */
3759
+ function notComponent(f, x) {
3760
+ if (!once(`SYG427:${f.name}`))
3761
+ return;
3762
+ const got = x === null ? 'null' : typeof x == 'object' ? 'an object' : String(x);
3763
+ devReport('SYG427', {
3764
+ component: f.name,
3765
+ message: `${f.name}'s intent calls CHILD.select(${got}): that is not a component, so it never emits`,
3766
+ 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() { … }",
3767
+ data: { got },
3768
+ });
3769
+ }
3697
3770
  /** the removed forms a definition shows (statics, view arity, model keys), once per function */
3698
3771
  let seenDefs = new WeakSet();
3699
3772
  // G-332: resetChecks() forgets them (as the once() dedupe)
@@ -4048,12 +4121,18 @@ function nextHooks(_api) {
4048
4121
  // G-527: one chain of SYG149 checks over the latest patches (G-501: it stops once every instance
4049
4122
  // that renders a named Collection is disposed, the app's dispose included)
4050
4123
  const vtCheck = vtChecker(() => vtp.size && on());
4124
+ // SYG428: the live instances by id, to read a child's parent state
4125
+ const live = new Map();
4051
4126
  return {
4052
4127
  onCreate(iv) {
4128
+ live.set(iv.id, iv);
4053
4129
  if (!on())
4054
4130
  return;
4055
4131
  log.onCreate(iv);
4056
4132
  const f = fac(iv);
4133
+ const parent = live.get(iv.parentId);
4134
+ if (parent)
4135
+ dottedKey(f, `<${f.name} state`, iv.props?.state, parent.state);
4057
4136
  removedStatics(f);
4058
4137
  if (!iv.def.intent)
4059
4138
  wire(iv, []);
@@ -4077,8 +4156,13 @@ function nextHooks(_api) {
4077
4156
  const v = t[k];
4078
4157
  if (k !== 'CHILD' || !v || typeof v.select != 'function')
4079
4158
  return v;
4080
- return { ...v, select: (x, ...r) => { if (typeof x == 'string')
4081
- removed(f, f.name, 'child-select-name', `CHILD.select('${x}')`); return v.select(x, ...r); } };
4159
+ return { ...v, select: (x, ...r) => {
4160
+ if (typeof x == 'string')
4161
+ removed(f, f.name, 'child-select-name', `CHILD.select('${x}')`);
4162
+ else if (typeof x != 'function')
4163
+ notComponent(f, x);
4164
+ return v.select(x, ...r);
4165
+ } };
4082
4166
  },
4083
4167
  });
4084
4168
  },
@@ -4107,6 +4191,7 @@ function nextHooks(_api) {
4107
4191
  },
4108
4192
  onDispose(iv) {
4109
4193
  vtp.delete(iv);
4194
+ live.delete(iv.id);
4110
4195
  H.onDispose(fac(iv));
4111
4196
  },
4112
4197
  onAction(iv, a) { log.onAction(iv, a); },
@@ -4154,6 +4239,7 @@ function nextHooks(_api) {
4154
4239
  checkCollection(f, { data: { props } });
4155
4240
  }
4156
4241
  catch (_) { /* ignore */ }
4242
+ dottedKey(f, `<${sel == 'collection' ? 'Collection' : 'VirtualCollection'} from`, props.from, owner.state);
4157
4243
  // G-419: SYG148 for a prefix that isn't an identifier; a valid one is checked for
4158
4244
  // duplicate names after each patch (SYG149). G-460: a falsy one names nothing (as the host)
4159
4245
  const p = props.viewTransitionName;