sygnal 6.4.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.
@@ -272,6 +272,8 @@ const CODE_TITLES = {
272
272
  // checks/next.ts and sygnal-check: a component reference that is not a function (an import of
273
273
  // a name the module doesn't export is undefined under Vitest)
274
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',
275
277
  // PLAN-9 (D470): checks/next.ts; dev entry
276
278
  SYG453: 'Two children with the same identity share one instance',
277
279
  SYG601: "Retired in 6.0: Missing or invalid sources",
@@ -480,6 +482,8 @@ const DEV_CODE_SEVERITY = {
480
482
  SYG426: 'warn',
481
483
  // checks/next.ts: CHILD.select(undefined), usually a missing export
482
484
  SYG427: 'error',
485
+ // checks/next.ts: a path designator that 6.4 read as one key with a dot
486
+ SYG428: 'warn',
483
487
  // PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
484
488
  SYG453: 'warn',
485
489
  SYG612: 'error',
@@ -1153,25 +1157,44 @@ function checkCollection(component, el) {
1153
1157
  state = component.addCalculated(state);
1154
1158
  }
1155
1159
  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`.
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)
1161
1163
  if (from !== undefined)
1162
1164
  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`))
1165
+ if (Array.isArray(state) || (state && Array.isArray(state.value)))
1166
+ return;
1167
+ if (!once(`SYG401:${name}:undefined`))
1167
1168
  return;
1168
1169
  reportSafely('SYG401', {
1169
1170
  component,
1170
- message: `<Collection of={${ofName}}> ${problem}, so it renders no items`,
1171
- 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`,
1172
1173
  data: { from: 'undefined', of: ofName },
1173
1174
  });
1174
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
+ }
1175
1198
  const WRAPPED = '__sygnalDiagWrapped';
1176
1199
  const collectionsCheck = {
1177
1200
  id: 'collections',
@@ -3736,6 +3759,22 @@ function removed(component, name, anchor, detail) {
3736
3759
  data: { form: anchor, guide: `${MIGRATE}#${anchor}`, ...(detail ? { detail } : {}) },
3737
3760
  });
3738
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
+ }
3739
3778
  /** SYG427: CHILD.select(x) with no component (an import of a name the module doesn't export) */
3740
3779
  function notComponent(f, x) {
3741
3780
  if (!once(`SYG427:${f.name}`))
@@ -4102,12 +4141,18 @@ function nextHooks(_api) {
4102
4141
  // G-527: one chain of SYG149 checks over the latest patches (G-501: it stops once every instance
4103
4142
  // that renders a named Collection is disposed, the app's dispose included)
4104
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();
4105
4146
  return {
4106
4147
  onCreate(iv) {
4148
+ live.set(iv.id, iv);
4107
4149
  if (!on())
4108
4150
  return;
4109
4151
  log.onCreate(iv);
4110
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);
4111
4156
  removedStatics(f);
4112
4157
  if (!iv.def.intent)
4113
4158
  wire(iv, []);
@@ -4166,6 +4211,7 @@ function nextHooks(_api) {
4166
4211
  },
4167
4212
  onDispose(iv) {
4168
4213
  vtp.delete(iv);
4214
+ live.delete(iv.id);
4169
4215
  H.onDispose(fac(iv));
4170
4216
  },
4171
4217
  onAction(iv, a) { log.onAction(iv, a); },
@@ -4213,6 +4259,7 @@ function nextHooks(_api) {
4213
4259
  checkCollection(f, { data: { props } });
4214
4260
  }
4215
4261
  catch (_) { /* ignore */ }
4262
+ dottedKey(f, `<${sel == 'collection' ? 'Collection' : 'VirtualCollection'} from`, props.from, owner.state);
4216
4263
  // G-419: SYG148 for a prefix that isn't an identifier; a valid one is checked for
4217
4264
  // duplicate names after each patch (SYG149). G-460: a falsy one names nothing (as the host)
4218
4265
  const p = props.viewTransitionName;