sygnal 6.3.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.
- package/CHANGELOG.md +91 -0
- package/dist/astro/index.cjs.js +69 -3
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +69 -3
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3 -2
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3 -2
- package/dist/astro/server.mjs.map +1 -1
- package/dist/diagnostics.cjs.js +105 -19
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +105 -19
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/mcp-apps.md +59 -4
- package/dist/index.cjs.js +54 -25
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +27 -6
- package/dist/index.esm.js +54 -25
- package/dist/index.esm.js.map +1 -1
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vite/plugin.cjs.js +69 -3
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +69 -3
- package/dist/vite/plugin.mjs.map +1 -1
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/src/core/cell.ts +25 -7
- package/src/core/hosts/collection.ts +6 -11
- package/src/core/hosts/switchable.ts +2 -2
- package/src/core/instance.ts +2 -2
- package/src/extra/diagnostics/checks/collections.ts +31 -25
- package/src/extra/diagnostics/checks/dom.ts +10 -2
- package/src/extra/diagnostics/checks/next.ts +39 -1
- package/src/extra/diagnostics/checks/persist.ts +4 -2
- package/src/extra/diagnostics/checks/wiring.ts +14 -6
- package/src/extra/diagnostics/codes.ts +9 -0
- package/src/extra/pwa.ts +3 -1
- package/src/extra/reducers.ts +10 -4
- package/src/index.d.ts +26 -5
- package/src/vite/plugin.ts +63 -2
package/dist/diagnostics.cjs.js
CHANGED
|
@@ -269,6 +269,11 @@ const CODE_TITLES = {
|
|
|
269
269
|
SYG425: 'isolatedState slice lacks initialState keys',
|
|
270
270
|
// PLAN-9 (D471): checks/next.ts; dev entry and sygnal-check
|
|
271
271
|
SYG426: 'Deprecated lowercase marker tag',
|
|
272
|
+
// checks/next.ts and sygnal-check: a component reference that is not a function (an import of
|
|
273
|
+
// a name the module doesn't export is undefined under Vitest)
|
|
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',
|
|
272
277
|
// PLAN-9 (D470): checks/next.ts; dev entry
|
|
273
278
|
SYG453: 'Two children with the same identity share one instance',
|
|
274
279
|
SYG601: "Retired in 6.0: Missing or invalid sources",
|
|
@@ -475,6 +480,10 @@ const DEV_CODE_SEVERITY = {
|
|
|
475
480
|
SYG425: 'warn',
|
|
476
481
|
// PLAN-9 (checks/next.ts): D471 a lowercase <collection> / <switchable> tag (deprecated in 6.3.0)
|
|
477
482
|
SYG426: 'warn',
|
|
483
|
+
// checks/next.ts: CHILD.select(undefined), usually a missing export
|
|
484
|
+
SYG427: 'error',
|
|
485
|
+
// checks/next.ts: a path designator that 6.4 read as one key with a dot
|
|
486
|
+
SYG428: 'warn',
|
|
478
487
|
// PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
|
|
479
488
|
SYG453: 'warn',
|
|
480
489
|
SYG612: 'error',
|
|
@@ -791,11 +800,20 @@ const underTest = (c) => {
|
|
|
791
800
|
const dom = c?.sources?.[c.DOMSourceName || 'DOM'];
|
|
792
801
|
return !!dom && typeof dom == 'object' && '_hub' in dom;
|
|
793
802
|
};
|
|
794
|
-
// G-620: an LLM / chat sink gets the chat driver's fix,
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
803
|
+
// G-620: the usual driver for a known sink name (an LLM / chat sink gets the chat driver's fix,
|
|
804
|
+
// SW the service worker's), the generic text for any other name
|
|
805
|
+
const DRIVER_FOR = [
|
|
806
|
+
[/^(LLM|CHAT|AI|ASSISTANT)$/, (s) => `run(App, { ${s}: makeChatDriver({ transport: openResponses({ baseURL: 'http://localhost:11434/v1', model: 'llama3.2' }) }) }) (from 'sygnal/ai'; uiMessageStream('/api/chat') for an AI SDK server)`],
|
|
807
|
+
[/^(SW|SERVICE_WORKER)$/, (s) => `run(App, { ${s}: makeServiceWorkerDriver('/sw.js', { enabled: import.meta.env.PROD }) }) (enabled: false keeps the driver, with no service worker, on a dev server)`],
|
|
808
|
+
[/^(HTTP|API|FETCH)$/, (s) => `run(App, { ${s}: makeFetchDriver() })`],
|
|
809
|
+
[/^(WS|SOCKET)$/, (s) => `run(App, { ${s}: makeSocketDriver() })`],
|
|
810
|
+
];
|
|
811
|
+
const driverFix = (sink) => {
|
|
812
|
+
const known = DRIVER_FOR.find(([re]) => re.test(sink));
|
|
813
|
+
return `Pass a driver named ${sink} to run(): ` +
|
|
814
|
+
(known ? known[1](sink) : `run(App, { ${sink}: makeFetchDriver() }) for HTTP, or driverFromAsync(fn) / your own driver`) +
|
|
815
|
+
`. Check the spelling against the drivers you pass`;
|
|
816
|
+
};
|
|
799
817
|
const wiringCheck = {
|
|
800
818
|
id: 'wiring',
|
|
801
819
|
onIntent(component, actionNames) {
|
|
@@ -1139,25 +1157,44 @@ function checkCollection(component, el) {
|
|
|
1139
1157
|
state = component.addCalculated(state);
|
|
1140
1158
|
}
|
|
1141
1159
|
catch (_) { }
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
//
|
|
1145
|
-
// the core itself (SYG401/SYG412, retrofitted in 1E) in every diagnostics mode (D22), so
|
|
1146
|
-
// 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)
|
|
1147
1163
|
if (from !== undefined)
|
|
1148
1164
|
return;
|
|
1149
|
-
if (
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1152
|
-
if (!problem || !once(`SYG401:${name}:undefined`))
|
|
1165
|
+
if (Array.isArray(state) || (state && Array.isArray(state.value)))
|
|
1166
|
+
return;
|
|
1167
|
+
if (!once(`SYG401:${name}:undefined`))
|
|
1153
1168
|
return;
|
|
1154
1169
|
reportSafely('SYG401', {
|
|
1155
1170
|
component,
|
|
1156
|
-
message: `<Collection of={${ofName}}> ${
|
|
1157
|
-
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`,
|
|
1158
1173
|
data: { from: 'undefined', of: ofName },
|
|
1159
1174
|
});
|
|
1160
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
|
+
}
|
|
1161
1198
|
const WRAPPED = '__sygnalDiagWrapped';
|
|
1162
1199
|
const collectionsCheck = {
|
|
1163
1200
|
id: 'collections',
|
|
@@ -1344,6 +1381,10 @@ onReset(() => {
|
|
|
1344
1381
|
});
|
|
1345
1382
|
const unref = (t) => { if (t && typeof t.unref === 'function')
|
|
1346
1383
|
t.unref(); return t; };
|
|
1384
|
+
/**
|
|
1385
|
+
* The view's source, plus the words sygnal/vite registered for the component (its module's and
|
|
1386
|
+
* its relative imports' string literals: markup a view helper draws), when it did
|
|
1387
|
+
*/
|
|
1347
1388
|
function viewSource(component) {
|
|
1348
1389
|
const view = component && component.view;
|
|
1349
1390
|
if (typeof view !== 'function')
|
|
@@ -1356,6 +1397,9 @@ function viewSource(component) {
|
|
|
1356
1397
|
catch (_) {
|
|
1357
1398
|
src = '';
|
|
1358
1399
|
}
|
|
1400
|
+
const registered = globalThis.__SYGNAL_VIEW_TOKENS__?.get?.(view);
|
|
1401
|
+
if (typeof registered == 'string')
|
|
1402
|
+
src += '\n' + registered;
|
|
1359
1403
|
viewTokens.set(view, src);
|
|
1360
1404
|
}
|
|
1361
1405
|
return src;
|
|
@@ -3000,7 +3044,8 @@ const persistCheck = {
|
|
|
3000
3044
|
const o = p.options, init = view.initialState;
|
|
3001
3045
|
if (!o || !init || typeof init != 'object')
|
|
3002
3046
|
return;
|
|
3003
|
-
|
|
3047
|
+
// a behavior's slice (`uses = { chat: chat() }`) is a state key too
|
|
3048
|
+
const keys = [...Object.keys(init), ...Object.keys(view.uses || {})];
|
|
3004
3049
|
for (const opt of ['pick', 'omit']) {
|
|
3005
3050
|
for (const k of Array.isArray(o[opt]) ? o[opt] : []) {
|
|
3006
3051
|
if (keys.includes(k) || !once(`SYG223:${name}:${opt}:${k}`))
|
|
@@ -3714,6 +3759,34 @@ function removed(component, name, anchor, detail) {
|
|
|
3714
3759
|
data: { form: anchor, guide: `${MIGRATE}#${anchor}`, ...(detail ? { detail } : {}) },
|
|
3715
3760
|
});
|
|
3716
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
|
+
}
|
|
3778
|
+
/** SYG427: CHILD.select(x) with no component (an import of a name the module doesn't export) */
|
|
3779
|
+
function notComponent(f, x) {
|
|
3780
|
+
if (!once(`SYG427:${f.name}`))
|
|
3781
|
+
return;
|
|
3782
|
+
const got = x === null ? 'null' : typeof x == 'object' ? 'an object' : String(x);
|
|
3783
|
+
devReport('SYG427', {
|
|
3784
|
+
component: f.name,
|
|
3785
|
+
message: `${f.name}'s intent calls CHILD.select(${got}): that is not a component, so it never emits`,
|
|
3786
|
+
fix: "Pass the child component itself, and check its import: a name its module doesn't export is undefined (Vitest runs on; the browser fails to link the module). Export it: export function Child() { … }",
|
|
3787
|
+
data: { got },
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3717
3790
|
/** the removed forms a definition shows (statics, view arity, model keys), once per function */
|
|
3718
3791
|
let seenDefs = new WeakSet();
|
|
3719
3792
|
// G-332: resetChecks() forgets them (as the once() dedupe)
|
|
@@ -4068,12 +4141,18 @@ function nextHooks(_api) {
|
|
|
4068
4141
|
// G-527: one chain of SYG149 checks over the latest patches (G-501: it stops once every instance
|
|
4069
4142
|
// that renders a named Collection is disposed, the app's dispose included)
|
|
4070
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();
|
|
4071
4146
|
return {
|
|
4072
4147
|
onCreate(iv) {
|
|
4148
|
+
live.set(iv.id, iv);
|
|
4073
4149
|
if (!on())
|
|
4074
4150
|
return;
|
|
4075
4151
|
log.onCreate(iv);
|
|
4076
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);
|
|
4077
4156
|
removedStatics(f);
|
|
4078
4157
|
if (!iv.def.intent)
|
|
4079
4158
|
wire(iv, []);
|
|
@@ -4097,8 +4176,13 @@ function nextHooks(_api) {
|
|
|
4097
4176
|
const v = t[k];
|
|
4098
4177
|
if (k !== 'CHILD' || !v || typeof v.select != 'function')
|
|
4099
4178
|
return v;
|
|
4100
|
-
return { ...v, select: (x, ...r) => {
|
|
4101
|
-
|
|
4179
|
+
return { ...v, select: (x, ...r) => {
|
|
4180
|
+
if (typeof x == 'string')
|
|
4181
|
+
removed(f, f.name, 'child-select-name', `CHILD.select('${x}')`);
|
|
4182
|
+
else if (typeof x != 'function')
|
|
4183
|
+
notComponent(f, x);
|
|
4184
|
+
return v.select(x, ...r);
|
|
4185
|
+
} };
|
|
4102
4186
|
},
|
|
4103
4187
|
});
|
|
4104
4188
|
},
|
|
@@ -4127,6 +4211,7 @@ function nextHooks(_api) {
|
|
|
4127
4211
|
},
|
|
4128
4212
|
onDispose(iv) {
|
|
4129
4213
|
vtp.delete(iv);
|
|
4214
|
+
live.delete(iv.id);
|
|
4130
4215
|
H.onDispose(fac(iv));
|
|
4131
4216
|
},
|
|
4132
4217
|
onAction(iv, a) { log.onAction(iv, a); },
|
|
@@ -4174,6 +4259,7 @@ function nextHooks(_api) {
|
|
|
4174
4259
|
checkCollection(f, { data: { props } });
|
|
4175
4260
|
}
|
|
4176
4261
|
catch (_) { /* ignore */ }
|
|
4262
|
+
dottedKey(f, `<${sel == 'collection' ? 'Collection' : 'VirtualCollection'} from`, props.from, owner.state);
|
|
4177
4263
|
// G-419: SYG148 for a prefix that isn't an identifier; a valid one is checked for
|
|
4178
4264
|
// duplicate names after each patch (SYG149). G-460: a falsy one names nothing (as the host)
|
|
4179
4265
|
const p = props.viewTransitionName;
|