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.esm.js
CHANGED
|
@@ -249,6 +249,11 @@ const CODE_TITLES = {
|
|
|
249
249
|
SYG425: 'isolatedState slice lacks initialState keys',
|
|
250
250
|
// PLAN-9 (D471): checks/next.ts; dev entry and sygnal-check
|
|
251
251
|
SYG426: 'Deprecated lowercase marker tag',
|
|
252
|
+
// checks/next.ts and sygnal-check: a component reference that is not a function (an import of
|
|
253
|
+
// a name the module doesn't export is undefined under Vitest)
|
|
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',
|
|
252
257
|
// PLAN-9 (D470): checks/next.ts; dev entry
|
|
253
258
|
SYG453: 'Two children with the same identity share one instance',
|
|
254
259
|
SYG601: "Retired in 6.0: Missing or invalid sources",
|
|
@@ -455,6 +460,10 @@ const DEV_CODE_SEVERITY = {
|
|
|
455
460
|
SYG425: 'warn',
|
|
456
461
|
// PLAN-9 (checks/next.ts): D471 a lowercase <collection> / <switchable> tag (deprecated in 6.3.0)
|
|
457
462
|
SYG426: 'warn',
|
|
463
|
+
// checks/next.ts: CHILD.select(undefined), usually a missing export
|
|
464
|
+
SYG427: 'error',
|
|
465
|
+
// checks/next.ts: a path designator that 6.4 read as one key with a dot
|
|
466
|
+
SYG428: 'warn',
|
|
458
467
|
// PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
|
|
459
468
|
SYG453: 'warn',
|
|
460
469
|
SYG612: 'error',
|
|
@@ -771,11 +780,20 @@ const underTest = (c) => {
|
|
|
771
780
|
const dom = c?.sources?.[c.DOMSourceName || 'DOM'];
|
|
772
781
|
return !!dom && typeof dom == 'object' && '_hub' in dom;
|
|
773
782
|
};
|
|
774
|
-
// G-620: an LLM / chat sink gets the chat driver's fix,
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
783
|
+
// G-620: the usual driver for a known sink name (an LLM / chat sink gets the chat driver's fix,
|
|
784
|
+
// SW the service worker's), the generic text for any other name
|
|
785
|
+
const DRIVER_FOR = [
|
|
786
|
+
[/^(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)`],
|
|
787
|
+
[/^(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)`],
|
|
788
|
+
[/^(HTTP|API|FETCH)$/, (s) => `run(App, { ${s}: makeFetchDriver() })`],
|
|
789
|
+
[/^(WS|SOCKET)$/, (s) => `run(App, { ${s}: makeSocketDriver() })`],
|
|
790
|
+
];
|
|
791
|
+
const driverFix = (sink) => {
|
|
792
|
+
const known = DRIVER_FOR.find(([re]) => re.test(sink));
|
|
793
|
+
return `Pass a driver named ${sink} to run(): ` +
|
|
794
|
+
(known ? known[1](sink) : `run(App, { ${sink}: makeFetchDriver() }) for HTTP, or driverFromAsync(fn) / your own driver`) +
|
|
795
|
+
`. Check the spelling against the drivers you pass`;
|
|
796
|
+
};
|
|
779
797
|
const wiringCheck = {
|
|
780
798
|
id: 'wiring',
|
|
781
799
|
onIntent(component, actionNames) {
|
|
@@ -1119,25 +1137,44 @@ function checkCollection(component, el) {
|
|
|
1119
1137
|
state = component.addCalculated(state);
|
|
1120
1138
|
}
|
|
1121
1139
|
catch (_) { }
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
//
|
|
1125
|
-
// the core itself (SYG401/SYG412, retrofitted in 1E) in every diagnostics mode (D22), so
|
|
1126
|
-
// 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)
|
|
1127
1143
|
if (from !== undefined)
|
|
1128
1144
|
return;
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
if (!problem || !once(`SYG401:${name}:undefined`))
|
|
1145
|
+
if (Array.isArray(state) || (state && Array.isArray(state.value)))
|
|
1146
|
+
return;
|
|
1147
|
+
if (!once(`SYG401:${name}:undefined`))
|
|
1133
1148
|
return;
|
|
1134
1149
|
reportSafely('SYG401', {
|
|
1135
1150
|
component,
|
|
1136
|
-
message: `<Collection of={${ofName}}> ${
|
|
1137
|
-
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`,
|
|
1138
1153
|
data: { from: 'undefined', of: ofName },
|
|
1139
1154
|
});
|
|
1140
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
|
+
}
|
|
1141
1178
|
const WRAPPED = '__sygnalDiagWrapped';
|
|
1142
1179
|
const collectionsCheck = {
|
|
1143
1180
|
id: 'collections',
|
|
@@ -1324,6 +1361,10 @@ onReset(() => {
|
|
|
1324
1361
|
});
|
|
1325
1362
|
const unref = (t) => { if (t && typeof t.unref === 'function')
|
|
1326
1363
|
t.unref(); return t; };
|
|
1364
|
+
/**
|
|
1365
|
+
* The view's source, plus the words sygnal/vite registered for the component (its module's and
|
|
1366
|
+
* its relative imports' string literals: markup a view helper draws), when it did
|
|
1367
|
+
*/
|
|
1327
1368
|
function viewSource(component) {
|
|
1328
1369
|
const view = component && component.view;
|
|
1329
1370
|
if (typeof view !== 'function')
|
|
@@ -1336,6 +1377,9 @@ function viewSource(component) {
|
|
|
1336
1377
|
catch (_) {
|
|
1337
1378
|
src = '';
|
|
1338
1379
|
}
|
|
1380
|
+
const registered = globalThis.__SYGNAL_VIEW_TOKENS__?.get?.(view);
|
|
1381
|
+
if (typeof registered == 'string')
|
|
1382
|
+
src += '\n' + registered;
|
|
1339
1383
|
viewTokens.set(view, src);
|
|
1340
1384
|
}
|
|
1341
1385
|
return src;
|
|
@@ -2980,7 +3024,8 @@ const persistCheck = {
|
|
|
2980
3024
|
const o = p.options, init = view.initialState;
|
|
2981
3025
|
if (!o || !init || typeof init != 'object')
|
|
2982
3026
|
return;
|
|
2983
|
-
|
|
3027
|
+
// a behavior's slice (`uses = { chat: chat() }`) is a state key too
|
|
3028
|
+
const keys = [...Object.keys(init), ...Object.keys(view.uses || {})];
|
|
2984
3029
|
for (const opt of ['pick', 'omit']) {
|
|
2985
3030
|
for (const k of Array.isArray(o[opt]) ? o[opt] : []) {
|
|
2986
3031
|
if (keys.includes(k) || !once(`SYG223:${name}:${opt}:${k}`))
|
|
@@ -3694,6 +3739,34 @@ function removed(component, name, anchor, detail) {
|
|
|
3694
3739
|
data: { form: anchor, guide: `${MIGRATE}#${anchor}`, ...(detail ? { detail } : {}) },
|
|
3695
3740
|
});
|
|
3696
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
|
+
}
|
|
3758
|
+
/** SYG427: CHILD.select(x) with no component (an import of a name the module doesn't export) */
|
|
3759
|
+
function notComponent(f, x) {
|
|
3760
|
+
if (!once(`SYG427:${f.name}`))
|
|
3761
|
+
return;
|
|
3762
|
+
const got = x === null ? 'null' : typeof x == 'object' ? 'an object' : String(x);
|
|
3763
|
+
devReport('SYG427', {
|
|
3764
|
+
component: f.name,
|
|
3765
|
+
message: `${f.name}'s intent calls CHILD.select(${got}): that is not a component, so it never emits`,
|
|
3766
|
+
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() { … }",
|
|
3767
|
+
data: { got },
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3697
3770
|
/** the removed forms a definition shows (statics, view arity, model keys), once per function */
|
|
3698
3771
|
let seenDefs = new WeakSet();
|
|
3699
3772
|
// G-332: resetChecks() forgets them (as the once() dedupe)
|
|
@@ -4048,12 +4121,18 @@ function nextHooks(_api) {
|
|
|
4048
4121
|
// G-527: one chain of SYG149 checks over the latest patches (G-501: it stops once every instance
|
|
4049
4122
|
// that renders a named Collection is disposed, the app's dispose included)
|
|
4050
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();
|
|
4051
4126
|
return {
|
|
4052
4127
|
onCreate(iv) {
|
|
4128
|
+
live.set(iv.id, iv);
|
|
4053
4129
|
if (!on())
|
|
4054
4130
|
return;
|
|
4055
4131
|
log.onCreate(iv);
|
|
4056
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);
|
|
4057
4136
|
removedStatics(f);
|
|
4058
4137
|
if (!iv.def.intent)
|
|
4059
4138
|
wire(iv, []);
|
|
@@ -4077,8 +4156,13 @@ function nextHooks(_api) {
|
|
|
4077
4156
|
const v = t[k];
|
|
4078
4157
|
if (k !== 'CHILD' || !v || typeof v.select != 'function')
|
|
4079
4158
|
return v;
|
|
4080
|
-
return { ...v, select: (x, ...r) => {
|
|
4081
|
-
|
|
4159
|
+
return { ...v, select: (x, ...r) => {
|
|
4160
|
+
if (typeof x == 'string')
|
|
4161
|
+
removed(f, f.name, 'child-select-name', `CHILD.select('${x}')`);
|
|
4162
|
+
else if (typeof x != 'function')
|
|
4163
|
+
notComponent(f, x);
|
|
4164
|
+
return v.select(x, ...r);
|
|
4165
|
+
} };
|
|
4082
4166
|
},
|
|
4083
4167
|
});
|
|
4084
4168
|
},
|
|
@@ -4107,6 +4191,7 @@ function nextHooks(_api) {
|
|
|
4107
4191
|
},
|
|
4108
4192
|
onDispose(iv) {
|
|
4109
4193
|
vtp.delete(iv);
|
|
4194
|
+
live.delete(iv.id);
|
|
4110
4195
|
H.onDispose(fac(iv));
|
|
4111
4196
|
},
|
|
4112
4197
|
onAction(iv, a) { log.onAction(iv, a); },
|
|
@@ -4154,6 +4239,7 @@ function nextHooks(_api) {
|
|
|
4154
4239
|
checkCollection(f, { data: { props } });
|
|
4155
4240
|
}
|
|
4156
4241
|
catch (_) { /* ignore */ }
|
|
4242
|
+
dottedKey(f, `<${sel == 'collection' ? 'Collection' : 'VirtualCollection'} from`, props.from, owner.state);
|
|
4157
4243
|
// G-419: SYG148 for a prefix that isn't an identifier; a valid one is checked for
|
|
4158
4244
|
// duplicate names after each patch (SYG149). G-460: a falsy one names nothing (as the host)
|
|
4159
4245
|
const p = props.viewTransitionName;
|