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.
@@ -117,6 +117,8 @@ const CODE_TITLES = {
117
117
  SYG111: 'Controlled input has no input listener',
118
118
  SYG115: 'Unknown DOM event shorthand',
119
119
  SYG116: 'EVENTS value has no string type',
120
+ // PLAN-13 N-1: dev entry (checks/next.ts onNext) and sygnal-check
121
+ SYG117: 'next() names an action with no model entry',
120
122
  SYG124: 'Component used as a control or selector',
121
123
  SYG125: 'Control given .intent, .model or .initialState',
122
124
  SYG126: 'Control rendered but never listened to',
@@ -152,6 +154,8 @@ const CODE_TITLES = {
152
154
  // PLAN-5 V-1: VirtualCollection (checks/virtual.ts)
153
155
  SYG430: 'VirtualCollection has no bounded height',
154
156
  SYG431: 'VirtualCollection items without ids',
157
+ // PLAN-13 F-3: dev entry (checks/next.ts onRender) and sygnal-check
158
+ SYG429: 'Switchable state map names no page',
155
159
  SYG432: 'VirtualCollection item does not render one element',
156
160
  SYG433: 'VirtualCollection scroll target not in the list',
157
161
  SYG434: 'Invalid VirtualCollection estimateSize or overscan',
@@ -167,6 +171,8 @@ const CODE_TITLES = {
167
171
  SYG202: 'STATE reducer returned undefined',
168
172
  // SYG3xx streams & intent construction (1A)
169
173
  SYG301: 'RxJS operator used on an xstream stream',
174
+ // PLAN-13 X-1: static only (sygnal-check)
175
+ SYG302: "Default import from 'xstream'",
170
176
  // SYG4xx collections / switchable / sub-components / context (1A)
171
177
  SYG401: "Collection 'from' field is missing or not an array",
172
178
  // SYG5xx strict mode / canonical forms (2A; dev-plans/PLAN-1-canonical-forms.md)
@@ -395,6 +401,7 @@ const DEV_CODE_SEVERITY = {
395
401
  SYG112: 'error',
396
402
  SYG115: 'warn',
397
403
  SYG116: 'error',
404
+ SYG117: 'warn',
398
405
  // PLAN-4 1-D: controls, static (sygnal-check)
399
406
  SYG124: 'error',
400
407
  SYG125: 'error',
@@ -406,6 +413,10 @@ const DEV_CODE_SEVERITY = {
406
413
  SYG129: 'warn',
407
414
  // PLAN-10 X-1: static only (sygnal-check)
408
415
  SYG170: 'warn',
416
+ // PLAN-13 X-1: static only (sygnal-check)
417
+ SYG302: 'info',
418
+ // PLAN-13 F-3 (checks/next.ts)
419
+ SYG429: 'warn',
409
420
  SYG171: 'warn',
410
421
  SYG221: 'error',
411
422
  // PLAN-4 GS-4 (state.ts)
@@ -1127,7 +1138,9 @@ const propsCheck = {
1127
1138
  data: { prop: key },
1128
1139
  });
1129
1140
  }
1130
- if ('state' in props) {
1141
+ // PLAN-13 F-3 follow-up: a Switchable page gets the Switchable's props, whose `state` is the
1142
+ // host's binding (a per-page map too), consumed by the host: not a prop meant for the page
1143
+ if ('state' in props && component.__next?.kind !== 'page') {
1131
1144
  const value = props.state;
1132
1145
  const isLens = typeof value === 'string' || (value && typeof value === 'object' && typeof value.get === 'function');
1133
1146
  if (!isLens && value !== undefined && once(`SYG106:${name}:state`)) {
@@ -3855,7 +3868,8 @@ const audited = (f, type, sink, h) => (s, d, nx, p, ...r) => {
3855
3868
  * page after a patch, G-460: on rendered elements), SYG612 (D173: a form
3856
3869
  * 6.0 removed, met at runtime; once per form and component, with a link to the migration
3857
3870
  * guide), SYG453 (PLAN-9, D470: two children with one identity share one instance), SYG426
3858
- * (PLAN-9, D471: a lowercase <collection> / <switchable> tag, deprecated in 6.3.0).
3871
+ * (PLAN-9, D471: a lowercase <collection> / <switchable> tag, deprecated in 6.3.0), SYG117
3872
+ * (PLAN-13 N-1: next() to an action the component has no model entry for).
3859
3873
  *
3860
3874
  * Only while diagnostics are on (the checks' own rule); production builds never load this.
3861
3875
  */
@@ -4051,6 +4065,36 @@ function lowercaseTags(iv) {
4051
4065
  };
4052
4066
  walk(raw.tmpl, 0);
4053
4067
  }
4068
+ /**
4069
+ * PLAN-13 F-3 SYG429: a Switchable's per-page `state={{ page: … }}` with a key that names no page
4070
+ * (read from the owner's template, as SYG426; 0 production bytes). Once per component and key
4071
+ */
4072
+ function switchableMaps(iv) {
4073
+ const raw = iv.i;
4074
+ if (!raw?.kids?.size || !raw.tmpl)
4075
+ return;
4076
+ const walk = (n, d) => {
4077
+ if (!n || typeof n != 'object' || n.$p || d > 300)
4078
+ return;
4079
+ const p = n.sel == 'switchable' && n.data?.props, st = p?.state;
4080
+ if (st && isPlainObject(st) && typeof st.get != 'function' && isPlainObject(p.of)) {
4081
+ const pages = Object.keys(p.of);
4082
+ for (const k of Object.keys(st))
4083
+ if (!(k in p.of) && once(`SYG429:${iv.name}:${k}`))
4084
+ devReport('SYG429', {
4085
+ component: iv.name,
4086
+ 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)}`,
4087
+ fix: 'Use the page name from of={{ … }} as the key, or remove the entry',
4088
+ data: { key: k, pages },
4089
+ });
4090
+ }
4091
+ const kids = n.children;
4092
+ if (Array.isArray(kids))
4093
+ for (const c of kids)
4094
+ walk(c, d + 1);
4095
+ };
4096
+ walk(raw.tmpl, 0);
4097
+ }
4054
4098
  /** the instance fields the checks read from today's Component instances */
4055
4099
  class Facade {
4056
4100
  constructor(__next) {
@@ -4327,6 +4371,7 @@ function nextHooks(_api) {
4327
4371
  removedTags(f, vnode);
4328
4372
  try {
4329
4373
  lowercaseTags(iv);
4374
+ switchableMaps(iv);
4330
4375
  }
4331
4376
  catch (_) { /* a check never breaks the app */ }
4332
4377
  // SYG453: only a view with children can render one twice
@@ -4369,6 +4414,26 @@ function nextHooks(_api) {
4369
4414
  }
4370
4415
  catch (_) { /* a check never breaks the app */ }
4371
4416
  },
4417
+ // SYG117 (PLAN-13 N-1): next('X') goes to the component's own model only; with no entry
4418
+ // for X it does nothing (an ancestor's entry of that name never runs)
4419
+ onNext(iv, type) {
4420
+ if (!on() || iv.def.handlers.has(type))
4421
+ return;
4422
+ const name = iv.name;
4423
+ if (!once(`SYG117:${name}:${type}`))
4424
+ return;
4425
+ let up = live.get(iv.parentId);
4426
+ while (up && !up.def.handlers.has(type))
4427
+ up = live.get(up.parentId);
4428
+ devReport('SYG117', {
4429
+ component: name,
4430
+ 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())}`,
4431
+ fix: up
4432
+ ? `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`
4433
+ : `Add the entry to ${name}.model (${type}: (state, data) => …), or fix the name to match an existing one`,
4434
+ data: { action: type, ...(up ? { handledBy: up.name } : {}) },
4435
+ });
4436
+ },
4372
4437
  onElementCommand(iv, cmd) {
4373
4438
  if (!on())
4374
4439
  return;