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.
@@ -252,6 +252,8 @@ const CODE_TITLES = {
252
252
  // checks/next.ts and sygnal-check: a component reference that is not a function (an import of
253
253
  // a name the module doesn't export is undefined under Vitest)
254
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',
255
257
  // PLAN-9 (D470): checks/next.ts; dev entry
256
258
  SYG453: 'Two children with the same identity share one instance',
257
259
  SYG601: "Retired in 6.0: Missing or invalid sources",
@@ -460,6 +462,8 @@ const DEV_CODE_SEVERITY = {
460
462
  SYG426: 'warn',
461
463
  // checks/next.ts: CHILD.select(undefined), usually a missing export
462
464
  SYG427: 'error',
465
+ // checks/next.ts: a path designator that 6.4 read as one key with a dot
466
+ SYG428: 'warn',
463
467
  // PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
464
468
  SYG453: 'warn',
465
469
  SYG612: 'error',
@@ -1133,25 +1137,44 @@ function checkCollection(component, el) {
1133
1137
  state = component.addCalculated(state);
1134
1138
  }
1135
1139
  catch (_) { }
1136
- let problem;
1137
- 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`;
1138
- // Every defined `from` (string field, lens object, or an invalid value) is reported by
1139
- // the core itself (SYG401/SYG412, retrofitted in 1E) in every diagnostics mode (D22), so
1140
- // 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)
1141
1143
  if (from !== undefined)
1142
1144
  return;
1143
- if (!Array.isArray(state) && !(state && Array.isArray(state.value))) {
1144
- problem = `has no 'from' prop, so it uses ${name}'s whole state, which is ${describe$1(state)}, not an array`;
1145
- }
1146
- if (!problem || !once(`SYG401:${name}:undefined`))
1145
+ if (Array.isArray(state) || (state && Array.isArray(state.value)))
1146
+ return;
1147
+ if (!once(`SYG401:${name}:undefined`))
1147
1148
  return;
1148
1149
  reportSafely('SYG401', {
1149
1150
  component,
1150
- message: `<Collection of={${ofName}}> ${problem}, so it renders no items`,
1151
- 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`,
1152
1153
  data: { from: 'undefined', of: ofName },
1153
1154
  });
1154
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
+ }
1155
1178
  const WRAPPED = '__sygnalDiagWrapped';
1156
1179
  const collectionsCheck = {
1157
1180
  id: 'collections',
@@ -3716,6 +3739,22 @@ function removed(component, name, anchor, detail) {
3716
3739
  data: { form: anchor, guide: `${MIGRATE}#${anchor}`, ...(detail ? { detail } : {}) },
3717
3740
  });
3718
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
+ }
3719
3758
  /** SYG427: CHILD.select(x) with no component (an import of a name the module doesn't export) */
3720
3759
  function notComponent(f, x) {
3721
3760
  if (!once(`SYG427:${f.name}`))
@@ -4082,12 +4121,18 @@ function nextHooks(_api) {
4082
4121
  // G-527: one chain of SYG149 checks over the latest patches (G-501: it stops once every instance
4083
4122
  // that renders a named Collection is disposed, the app's dispose included)
4084
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();
4085
4126
  return {
4086
4127
  onCreate(iv) {
4128
+ live.set(iv.id, iv);
4087
4129
  if (!on())
4088
4130
  return;
4089
4131
  log.onCreate(iv);
4090
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);
4091
4136
  removedStatics(f);
4092
4137
  if (!iv.def.intent)
4093
4138
  wire(iv, []);
@@ -4146,6 +4191,7 @@ function nextHooks(_api) {
4146
4191
  },
4147
4192
  onDispose(iv) {
4148
4193
  vtp.delete(iv);
4194
+ live.delete(iv.id);
4149
4195
  H.onDispose(fac(iv));
4150
4196
  },
4151
4197
  onAction(iv, a) { log.onAction(iv, a); },
@@ -4193,6 +4239,7 @@ function nextHooks(_api) {
4193
4239
  checkCollection(f, { data: { props } });
4194
4240
  }
4195
4241
  catch (_) { /* ignore */ }
4242
+ dottedKey(f, `<${sel == 'collection' ? 'Collection' : 'VirtualCollection'} from`, props.from, owner.state);
4196
4243
  // G-419: SYG148 for a prefix that isn't an identifier; a valid one is checked for
4197
4244
  // duplicate names after each patch (SYG149). G-460: a falsy one names nothing (as the host)
4198
4245
  const p = props.viewTransitionName;