sygnal 6.6.0 → 7.0.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 (82) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +2 -0
  3. package/dist/astro/index.cjs.js +32 -9
  4. package/dist/astro/index.cjs.js.map +1 -1
  5. package/dist/astro/index.mjs +32 -9
  6. package/dist/astro/index.mjs.map +1 -1
  7. package/dist/astro/server.cjs.js.map +1 -1
  8. package/dist/astro/server.mjs.map +1 -1
  9. package/dist/devtools.cjs.js.map +1 -1
  10. package/dist/devtools.esm.js.map +1 -1
  11. package/dist/diagnostics.cjs.js +87 -4
  12. package/dist/diagnostics.cjs.js.map +1 -1
  13. package/dist/diagnostics.esm.js +86 -5
  14. package/dist/diagnostics.esm.js.map +1 -1
  15. package/dist/guide/accessibility.md +2 -0
  16. package/dist/guide/ai-chat.md +6 -6
  17. package/dist/guide/behaviors.md +1 -1
  18. package/dist/guide/element-commands.md +4 -2
  19. package/dist/guide/forms-reference.md +89 -3
  20. package/dist/guide/forms.md +8 -8
  21. package/dist/guide/http.md +77 -7
  22. package/dist/guide/migrating-to-7.md +314 -0
  23. package/dist/guide/recipes/ai-summarize.md +2 -1
  24. package/dist/guide/recipes/i18n.md +1 -1
  25. package/dist/guide/ui/tabs.md +5 -4
  26. package/dist/guide/ui/tooltip.md +24 -0
  27. package/dist/index.cjs.js +772 -318
  28. package/dist/index.cjs.js.map +1 -1
  29. package/dist/index.d.ts +198 -31
  30. package/dist/index.esm.js +772 -318
  31. package/dist/index.esm.js.map +1 -1
  32. package/dist/sygnal.min.js +1 -1
  33. package/dist/sygnal.min.js.map +1 -1
  34. package/dist/ui.cjs.js +96 -11
  35. package/dist/ui.cjs.js.map +1 -1
  36. package/dist/ui.esm.js +96 -12
  37. package/dist/ui.esm.js.map +1 -1
  38. package/dist/vike/config/package.json +1 -1
  39. package/dist/vite/plugin.cjs.js +19 -9
  40. package/dist/vite/plugin.cjs.js.map +1 -1
  41. package/dist/vite/plugin.mjs +19 -9
  42. package/dist/vite/plugin.mjs.map +1 -1
  43. package/llms.txt +17 -16
  44. package/package.json +1 -1
  45. package/src/astro/index.d.ts +8 -0
  46. package/src/astro/index.ts +18 -0
  47. package/src/core/actions.ts +28 -21
  48. package/src/core/define.ts +2 -0
  49. package/src/core/hooks.ts +2 -2
  50. package/src/core/hosts/collection.ts +4 -3
  51. package/src/core/instance.ts +60 -16
  52. package/src/core/runtime.ts +5 -1
  53. package/src/extra/ai/chat/behavior.ts +3 -3
  54. package/src/extra/ai/commandBar.ts +3 -3
  55. package/src/extra/ai/compose/behavior.ts +3 -3
  56. package/src/extra/ai/transports/shared.ts +8 -2
  57. package/src/extra/behaviors.ts +21 -0
  58. package/src/extra/diagnostics/checks/fetch.ts +21 -1
  59. package/src/extra/diagnostics/checks/forms.ts +4 -3
  60. package/src/extra/diagnostics/checks/index.ts +1 -0
  61. package/src/extra/diagnostics/checks/next.ts +3 -0
  62. package/src/extra/diagnostics/checks/public.d.ts +12 -0
  63. package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
  64. package/src/extra/diagnostics/codes.ts +13 -0
  65. package/src/extra/fetchDriver.ts +112 -7
  66. package/src/extra/fetchEventSource.ts +113 -0
  67. package/src/extra/form.ts +68 -26
  68. package/src/extra/formHelpers.ts +3 -1
  69. package/src/extra/lens.ts +3 -2
  70. package/src/extra/queryCache.ts +4 -3
  71. package/src/extra/socketDriver.ts +10 -4
  72. package/src/extra/testing.ts +68 -11
  73. package/src/index.d.ts +197 -30
  74. package/src/index.ts +1 -0
  75. package/src/shared.ts +3 -0
  76. package/src/ui/dialog.ts +3 -2
  77. package/src/ui/tooltips.ts +92 -0
  78. package/src/ui.d.ts +35 -0
  79. package/src/ui.ts +1 -0
  80. package/src/vite/graph.ts +11 -3
  81. package/src/vite/plugin.d.ts +7 -0
  82. package/src/vite/plugin.ts +10 -5
@@ -301,6 +301,8 @@ const CODE_TITLES = {
301
301
  SYG634: 'latest: true with a computed key',
302
302
  // PLAN-3 5-5 (D88)
303
303
  SYG635: 'Caching asked for without a queryCache',
304
+ // PLAN-11 U-1: progress requests (dev entry, fetch.ts)
305
+ SYG636: "progress that can't be reported as asked",
304
306
  // PLAN-4 GS-2: element commands (reported by the dev entry, checks/elementCommands.ts)
305
307
  SYG640: 'Element command target not found',
306
308
  SYG641: 'Unknown element command',
@@ -360,6 +362,9 @@ const CODE_TITLES = {
360
362
  // PLAN-9 X-2: static only (sygnal-check rules/syg253-catalog.js)
361
363
  SYG253: 'Misspelled catalog declaration key',
362
364
  SYG254: 'Catalog descriptions or options overlap',
365
+ SYG256: 'Output guard rejects the updated state',
366
+ SYG257: 'Output reads a field its action changed (7.0 timing audit)',
367
+ SYG454: 'State binding changed without a new component identity',
363
368
  // PLAN-9 H: chat client tools and ask (src/extra/ai/chat/behavior.ts, compose/ask.ts, compose/render.ts)
364
369
  SYG160: 'Result part rendered outside a card',
365
370
  SYG684: 'Client tool shadows an agent tool',
@@ -470,6 +475,8 @@ const DEV_CODE_SEVERITY = {
470
475
  SYG634: 'info',
471
476
  // PLAN-3 5-5 (fetch.ts)
472
477
  SYG635: 'warn',
478
+ // PLAN-11 U-1 (fetch.ts)
479
+ SYG636: 'warn',
473
480
  // PLAN-4 GS-2 (checks/elementCommands.ts, through the core bridge; renderComponent too)
474
481
  SYG640: 'warn',
475
482
  SYG641: 'error',
@@ -586,6 +593,12 @@ const DEV_CODE_SEVERITY = {
586
593
  // PLAN-9 X-2: static only (sygnal-check); listed so the entry knows their titles
587
594
  SYG253: 'warn',
588
595
  SYG254: 'info',
596
+ // PLAN-12: static only; proven after-state output guard contradiction.
597
+ SYG256: 'warn',
598
+ // 7.0: the opt-in timing audit (checks/next.ts; configureTimingAudit)
599
+ SYG257: 'info',
600
+ // PLAN-12: static only for legacy (6.x) binding contracts. Sygnal 7 remounts known targets.
601
+ SYG454: 'warn',
589
602
  };
590
603
  function getCodeInfo(code) {
591
604
  const severity = CODE_SEVERITY[code];
@@ -2211,14 +2224,15 @@ const formsCheck = {
2211
2224
  });
2212
2225
  const model = Object.keys(view.model || {}).map(a => a.split('|')[0].trim()), own = ownActions(b);
2213
2226
  const clash = own.includes(o.submit);
2214
- if (typeof o.submit != 'string' || !model.includes(o.submit) || clash) {
2227
+ // PLAN-11 F-3: with `request`, the form sends the submit itself and `submit` is optional
2228
+ if (o.request ? o.submit !== undefined && (typeof o.submit != 'string' || !model.includes(o.submit) || clash) : typeof o.submit != 'string' || !model.includes(o.submit) || clash) {
2215
2229
  devReport('SYG234', {
2216
2230
  component,
2217
- message: typeof o.submit != 'string' ? `${name}'s form '${key}' has no submit action, so a valid submit dispatches nothing`
2231
+ message: typeof o.submit != 'string' ? `${name}'s form '${key}' has no submit action and no request, so a valid submit dispatches nothing`
2218
2232
  : clash ? `${name}'s form '${key}' names submit: '${o.submit}', one of the form's own actions; a valid submit dispatches the form's '${key}.${o.submit}'${model.includes(o.submit) ? `, never ${name}'s '${o.submit}' entry` : ''}`
2219
2233
  : `${name}'s form '${key}' dispatches '${o.submit}' on a valid submit, but ${name} has no model entry '${o.submit}'`,
2220
2234
  fix: clash ? SUBMIT_FIX(key, o.submit, own)
2221
- : `Name a host action and add it to the model: submit: 'SIGN_UP', model: { SIGN_UP: { HTTP: (state, values) => ({ url, json: values, ok: '${key}.DONE', error: '${key}.ERRORS' }) } }`,
2235
+ : `Give the form a request, form(schema, { values, request: { url: '/api/signup' } }) (it sends the valid submit itself), or name a host action and add it to the model: submit: 'SIGN_UP', model: { SIGN_UP: { HTTP: (state, values) => ({ url, json: values, ok: '${key}.DONE', error: '${key}.ERRORS' }) } }`,
2222
2236
  data: { key, submit: o.submit },
2223
2237
  });
2224
2238
  }
@@ -2643,6 +2657,13 @@ const routerCheck = {
2643
2657
  const isStandardSchema = (schema) => !!schema && (typeof schema == 'object' || typeof schema == 'function') && typeof schema['~standard']?.validate == 'function';
2644
2658
 
2645
2659
  const IDEMPOTENT = /^(GET|HEAD)$/i;
2660
+ // PLAN-11 U-1: fetch() init options a progress request (XMLHttpRequest) can't honour
2661
+ const XHR_IGNORES = ['keepalive', 'priority', 'cache', 'redirect', 'integrity', 'mode', 'referrer', 'referrerPolicy', 'window', 'duplex'];
2662
+ const reportProgress = (component, sink, url, why, fix) => {
2663
+ if (!once(`SYG636:${nameOf(component)}:${url}:${why}`))
2664
+ return;
2665
+ devReport('SYG636', { component, message: `${sink} ${url ? url + ' ' : ''}has progress, but ${why}`, fix, data: { sink, url } });
2666
+ };
2646
2667
  /** instance → reply key lanes it has sent, and ok name → the other key it was sent under */
2647
2668
  const lanes = new WeakMap();
2648
2669
  const fetchSink = (component, name) => component?.sources?.[name]?.__sygnalStatic === 'resources';
@@ -2666,7 +2687,7 @@ function checkOne(component, sink, req, where) {
2666
2687
  return;
2667
2688
  const name = nameOf(component);
2668
2689
  const url = typeof req.url === 'string' ? req.url : '';
2669
- const method = String(req.method || (req.json !== undefined || req.body !== undefined ? 'POST' : 'GET')).toUpperCase();
2690
+ const method = String(req.method || (req.json !== undefined || req.body !== undefined || req.multipart !== undefined ? 'POST' : 'GET')).toUpperCase();
2670
2691
  if ((req.cache === true || req.staleTime != null) && !IDEMPOTENT.test(method) && once(`SYG630:${name}:${method}:${url}`)) {
2671
2692
  devReport('SYG630', {
2672
2693
  component,
@@ -2677,6 +2698,17 @@ function checkOne(component, sink, req, where) {
2677
2698
  }
2678
2699
  if (req.cache === true || req.staleTime != null)
2679
2700
  reportNoCache(component, sink, req.cache === true ? 'cache: true' : 'staleTime', url);
2701
+ if (req.progress) {
2702
+ if (where.includes('.resources.'))
2703
+ reportProgress(component, sink, url, 'resources report no progress (it is ignored)', 'Send the download as a request with progress and reply actions instead of a resource, or remove progress');
2704
+ else if (req.batch)
2705
+ reportProgress(component, sink, url, 'a batch request reports no progress (it is ignored)', 'Send the parts as separate requests with progress, or remove progress');
2706
+ else {
2707
+ const bad = XHR_IGNORES.filter(k => req.init && req.init[k] !== undefined);
2708
+ if (bad.length)
2709
+ reportProgress(component, sink, url, `it goes over XMLHttpRequest, which ignores init.${bad.join(', init.')}`, `Remove ${bad.join(', ')} from init for this request, or drop progress if the request needs ${bad.length > 1 ? 'them' : 'it'}`);
2710
+ }
2711
+ }
2680
2712
  if ('validate' in req && !isStandardSchema(req.validate) && once(`SYG631:${name}:${url}`)) {
2681
2713
  devReport('SYG631', {
2682
2714
  component,
@@ -3777,6 +3809,52 @@ function installInspect() {
3777
3809
  };
3778
3810
  }
3779
3811
 
3812
+ /**
3813
+ * The 7.0 timing audit (SYG257, info): since 7.0 a model entry's STATE runs first and every other
3814
+ * sink gets the state it committed (6.x gave them the state from before the action). For code
3815
+ * written for 6.x, this reports each output sink that reads a top-level state field its action's
3816
+ * STATE changed: it gets the new value now. Fix: read `props.previousState.<field>` where the old
3817
+ * value is meant; a read meant to see the new value needs nothing. Opt-in, because in 7.0-native
3818
+ * code reading the new value is the intent. Turn it on with any of:
3819
+ * - configureTimingAudit(true) (this entry)
3820
+ * - renderComponent(C, { timingAudit: true }) (tests)
3821
+ * - sygnal({ diagnostics: { timingAudit: true } }) (sygnal/vite, dev)
3822
+ * - globalThis.__SYGNAL_TIMING_AUDIT__ = true
3823
+ * The flag lives on the core bridge (shared by every copy of this entry and renderComponent).
3824
+ * Mechanism: the dev hook layer's wrapHandler (checks/next.ts) wraps each non-STATE handler and
3825
+ * gives it the state through a Proxy that notes reads of changed top-level fields. Top-level
3826
+ * fields only (a nested change shows as its top-level field changing); once per component name,
3827
+ * action, sink and field. Nothing in production builds.
3828
+ */
3829
+ /** Turn the timing audit on (true), off (false), or back to the default (globalThis.__SYGNAL_TIMING_AUDIT__). */
3830
+ function configureTimingAudit(on) {
3831
+ const core = bridge();
3832
+ if (core)
3833
+ core.timingAudit = on;
3834
+ }
3835
+ /** Whether the timing audit is on. */
3836
+ function isTimingAuditEnabled() {
3837
+ const flag = bridge()?.timingAudit;
3838
+ return flag === undefined ? globalThis.__SYGNAL_TIMING_AUDIT__ === true : flag === true;
3839
+ }
3840
+ /** handler `h` of `f`'s action `type`, sink `sink`, audited (f: the checks' instance facade) */
3841
+ const audited = (f, type, sink, h) => (s, d, nx, p, ...r) => {
3842
+ const was = p?.previousState;
3843
+ if (!isTimingAuditEnabled() || !was || was === s || !s || typeof s != 'object' || typeof was != 'object' || Array.isArray(s))
3844
+ return h(s, d, nx, p, ...r);
3845
+ const view = new Proxy(s, { get(t, k, rcv) {
3846
+ if (typeof k == 'string' && k in was && was[k] !== t[k] && once(`SYG257:${f.name}:${type}:${sink}:${k}`))
3847
+ devReport('SYG257', {
3848
+ component: f,
3849
+ message: `${f.name}'s '${type}' ${sink} read state.${k}, which the same action's STATE changed: since 7.0 it gets the new value (6.x gave it the old one)`,
3850
+ fix: `If it meant the value from before the action, read props.previousState.${k} (the 4th argument); if it meant the new value, nothing to change`,
3851
+ data: { action: type, sink, field: k },
3852
+ });
3853
+ return Reflect.get(t, k, rcv);
3854
+ } });
3855
+ return h(view, d, nx, p, ...r);
3856
+ };
3857
+
3780
3858
  /**
3781
3859
  * PLAN-4.6 R4: the dev entry's checks on the next component core (src/core/).
3782
3860
  *
@@ -4289,6 +4367,9 @@ function nextHooks(_api) {
4289
4367
  onAction(iv, a) { log.onAction(iv, a); },
4290
4368
  wrapHandler(iv, type, sink, fn) {
4291
4369
  let h = log.wrapHandler(iv, type, sink, fn) || fn;
4370
+ // 7.0 timing audit (SYG257, opt-in; checks/timingAudit.ts): checked per call, so turning it on later counts
4371
+ if (on() && sink != 'STATE' && typeof h == 'function')
4372
+ h = audited(fac(iv), type, sink, h);
4292
4373
  // SYG222: the STATE reducers, wrapped (today the check rewrote the instance's model)
4293
4374
  if (on() && sink == 'STATE' && typeof fn == 'function')
4294
4375
  h = watchMutation(fac(iv), type, h);
@@ -4508,10 +4589,12 @@ exports.checkEventBus = checkEventBus;
4508
4589
  exports.checks = checks;
4509
4590
  exports.configureChecks = configureChecks;
4510
4591
  exports.configureStrict = configureStrict;
4592
+ exports.configureTimingAudit = configureTimingAudit;
4511
4593
  exports.getCodeInfo = getCodeInfo;
4512
4594
  exports.inspect = inspect;
4513
4595
  exports.installChecks = installChecks;
4514
4596
  exports.isStrictEnabled = isStrictEnabled;
4597
+ exports.isTimingAuditEnabled = isTimingAuditEnabled;
4515
4598
  exports.listCodes = listCodes;
4516
4599
  exports.resetChecks = resetChecks;
4517
4600
  //# sourceMappingURL=diagnostics.cjs.js.map