sygnal 7.0.0 → 7.1.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.
@@ -137,6 +137,8 @@ const CODE_TITLES = {
137
137
  SYG111: 'Controlled input has no input listener',
138
138
  SYG115: 'Unknown DOM event shorthand',
139
139
  SYG116: 'EVENTS value has no string type',
140
+ // PLAN-13 N-1: dev entry (checks/next.ts onNext) and sygnal-check
141
+ SYG117: 'next() names an action with no model entry',
140
142
  SYG124: 'Component used as a control or selector',
141
143
  SYG125: 'Control given .intent, .model or .initialState',
142
144
  SYG126: 'Control rendered but never listened to',
@@ -172,6 +174,8 @@ const CODE_TITLES = {
172
174
  // PLAN-5 V-1: VirtualCollection (checks/virtual.ts)
173
175
  SYG430: 'VirtualCollection has no bounded height',
174
176
  SYG431: 'VirtualCollection items without ids',
177
+ // PLAN-13 F-3: dev entry (checks/next.ts onRender) and sygnal-check
178
+ SYG429: 'Switchable state map names no page',
175
179
  SYG432: 'VirtualCollection item does not render one element',
176
180
  SYG433: 'VirtualCollection scroll target not in the list',
177
181
  SYG434: 'Invalid VirtualCollection estimateSize or overscan',
@@ -187,6 +191,8 @@ const CODE_TITLES = {
187
191
  SYG202: 'STATE reducer returned undefined',
188
192
  // SYG3xx streams & intent construction (1A)
189
193
  SYG301: 'RxJS operator used on an xstream stream',
194
+ // PLAN-13 X-1: static only (sygnal-check)
195
+ SYG302: "Default import from 'xstream'",
190
196
  // SYG4xx collections / switchable / sub-components / context (1A)
191
197
  SYG401: "Collection 'from' field is missing or not an array",
192
198
  // SYG5xx strict mode / canonical forms (2A; dev-plans/PLAN-1-canonical-forms.md)
@@ -415,6 +421,7 @@ const DEV_CODE_SEVERITY = {
415
421
  SYG112: 'error',
416
422
  SYG115: 'warn',
417
423
  SYG116: 'error',
424
+ SYG117: 'warn',
418
425
  // PLAN-4 1-D: controls, static (sygnal-check)
419
426
  SYG124: 'error',
420
427
  SYG125: 'error',
@@ -426,6 +433,10 @@ const DEV_CODE_SEVERITY = {
426
433
  SYG129: 'warn',
427
434
  // PLAN-10 X-1: static only (sygnal-check)
428
435
  SYG170: 'warn',
436
+ // PLAN-13 X-1: static only (sygnal-check)
437
+ SYG302: 'info',
438
+ // PLAN-13 F-3 (checks/next.ts)
439
+ SYG429: 'warn',
429
440
  SYG171: 'warn',
430
441
  SYG221: 'error',
431
442
  // PLAN-4 GS-4 (state.ts)
@@ -1147,7 +1158,9 @@ const propsCheck = {
1147
1158
  data: { prop: key },
1148
1159
  });
1149
1160
  }
1150
- if ('state' in props) {
1161
+ // PLAN-13 F-3 follow-up: a Switchable page gets the Switchable's props, whose `state` is the
1162
+ // host's binding (a per-page map too), consumed by the host: not a prop meant for the page
1163
+ if ('state' in props && component.__next?.kind !== 'page') {
1151
1164
  const value = props.state;
1152
1165
  const isLens = typeof value === 'string' || (value && typeof value === 'object' && typeof value.get === 'function');
1153
1166
  if (!isLens && value !== undefined && once(`SYG106:${name}:state`)) {
@@ -3875,7 +3888,8 @@ const audited = (f, type, sink, h) => (s, d, nx, p, ...r) => {
3875
3888
  * page after a patch, G-460: on rendered elements), SYG612 (D173: a form
3876
3889
  * 6.0 removed, met at runtime; once per form and component, with a link to the migration
3877
3890
  * guide), SYG453 (PLAN-9, D470: two children with one identity share one instance), SYG426
3878
- * (PLAN-9, D471: a lowercase <collection> / <switchable> tag, deprecated in 6.3.0).
3891
+ * (PLAN-9, D471: a lowercase <collection> / <switchable> tag, deprecated in 6.3.0), SYG117
3892
+ * (PLAN-13 N-1: next() to an action the component has no model entry for).
3879
3893
  *
3880
3894
  * Only while diagnostics are on (the checks' own rule); production builds never load this.
3881
3895
  */
@@ -4071,6 +4085,36 @@ function lowercaseTags(iv) {
4071
4085
  };
4072
4086
  walk(raw.tmpl, 0);
4073
4087
  }
4088
+ /**
4089
+ * PLAN-13 F-3 SYG429: a Switchable's per-page `state={{ page: … }}` with a key that names no page
4090
+ * (read from the owner's template, as SYG426; 0 production bytes). Once per component and key
4091
+ */
4092
+ function switchableMaps(iv) {
4093
+ const raw = iv.i;
4094
+ if (!raw?.kids?.size || !raw.tmpl)
4095
+ return;
4096
+ const walk = (n, d) => {
4097
+ if (!n || typeof n != 'object' || n.$p || d > 300)
4098
+ return;
4099
+ const p = n.sel == 'switchable' && n.data?.props, st = p?.state;
4100
+ if (st && isPlainObject(st) && typeof st.get != 'function' && isPlainObject(p.of)) {
4101
+ const pages = Object.keys(p.of);
4102
+ for (const k of Object.keys(st))
4103
+ if (!(k in p.of) && once(`SYG429:${iv.name}:${k}`))
4104
+ devReport('SYG429', {
4105
+ component: iv.name,
4106
+ message: `${iv.name}'s Switchable state map binds '${k}', which is not one of its pages (${pages.join(', ')}), so that binding is never used${didYouMean(k, pages)}`,
4107
+ fix: 'Use the page name from of={{ … }} as the key, or remove the entry',
4108
+ data: { key: k, pages },
4109
+ });
4110
+ }
4111
+ const kids = n.children;
4112
+ if (Array.isArray(kids))
4113
+ for (const c of kids)
4114
+ walk(c, d + 1);
4115
+ };
4116
+ walk(raw.tmpl, 0);
4117
+ }
4074
4118
  /** the instance fields the checks read from today's Component instances */
4075
4119
  class Facade {
4076
4120
  constructor(__next) {
@@ -4347,6 +4391,7 @@ function nextHooks(_api) {
4347
4391
  removedTags(f, vnode);
4348
4392
  try {
4349
4393
  lowercaseTags(iv);
4394
+ switchableMaps(iv);
4350
4395
  }
4351
4396
  catch (_) { /* a check never breaks the app */ }
4352
4397
  // SYG453: only a view with children can render one twice
@@ -4389,6 +4434,26 @@ function nextHooks(_api) {
4389
4434
  }
4390
4435
  catch (_) { /* a check never breaks the app */ }
4391
4436
  },
4437
+ // SYG117 (PLAN-13 N-1): next('X') goes to the component's own model only; with no entry
4438
+ // for X it does nothing (an ancestor's entry of that name never runs)
4439
+ onNext(iv, type) {
4440
+ if (!on() || iv.def.handlers.has(type))
4441
+ return;
4442
+ const name = iv.name;
4443
+ if (!once(`SYG117:${name}:${type}`))
4444
+ return;
4445
+ let up = live.get(iv.parentId);
4446
+ while (up && !up.def.handlers.has(type))
4447
+ up = live.get(up.parentId);
4448
+ devReport('SYG117', {
4449
+ component: name,
4450
+ message: `${name} calls next('${type}'), but ${name} has no model entry '${type}', so the action does nothing${up ? `: next() never reaches ${up.name}, which handles '${type}'` : ''}${didYouMean(type, iv.def.handlers.keys())}`,
4451
+ fix: up
4452
+ ? `next() stays in its own component. Send it up instead: give the entry that calls it a PARENT sink (PARENT: (state) => ({ … })) and add ${type}: CHILD.select(${name}) to ${up.name}.intent, or emit EVENTS: event('${type}', …) and select it there`
4453
+ : `Add the entry to ${name}.model (${type}: (state, data) => …), or fix the name to match an existing one`,
4454
+ data: { action: type, ...(up ? { handledBy: up.name } : {}) },
4455
+ });
4456
+ },
4392
4457
  onElementCommand(iv, cmd) {
4393
4458
  if (!on())
4394
4459
  return;