@tiaforge-ui/sinter 0.0.0-stage → 0.2.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/LICENSE +21 -0
  2. package/README.md +158 -2
  3. package/dist/cache.d.ts +5 -0
  4. package/dist/cache.js +95 -0
  5. package/dist/check/index.d.ts +85 -0
  6. package/dist/check/index.js +156 -0
  7. package/dist/check/openapi-types.d.ts +6 -0
  8. package/dist/check/openapi-types.js +84 -0
  9. package/dist/check/shadow.d.ts +49 -0
  10. package/dist/check/shadow.js +279 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +67 -0
  13. package/dist/compiler/check-unit.d.ts +109 -0
  14. package/dist/compiler/check-unit.js +3 -0
  15. package/dist/compiler/compile.d.ts +66 -0
  16. package/dist/compiler/compile.js +1316 -0
  17. package/dist/compiler/diagnostic-docs.d.ts +10 -0
  18. package/dist/compiler/diagnostic-docs.js +142 -0
  19. package/dist/compiler/diagnostics.d.ts +57 -0
  20. package/dist/compiler/diagnostics.js +77 -0
  21. package/dist/compiler/expr.d.ts +97 -0
  22. package/dist/compiler/expr.js +853 -0
  23. package/dist/compiler/index.d.ts +22 -0
  24. package/dist/compiler/index.js +160 -0
  25. package/dist/compiler/openapi.d.ts +40 -0
  26. package/dist/compiler/openapi.js +177 -0
  27. package/dist/compiler/vocabulary.d.ts +12 -0
  28. package/dist/compiler/vocabulary.js +39 -0
  29. package/dist/dev.d.ts +10 -0
  30. package/dist/dev.js +92 -0
  31. package/dist/each.d.ts +3 -0
  32. package/dist/each.js +121 -0
  33. package/dist/index.d.ts +7 -0
  34. package/dist/index.js +4 -0
  35. package/dist/ir.d.ts +152 -0
  36. package/dist/ir.js +121 -0
  37. package/dist/lsp/quickfix.d.ts +15 -0
  38. package/dist/lsp/quickfix.js +58 -0
  39. package/dist/lsp/rpc.d.ts +31 -0
  40. package/dist/lsp/rpc.js +120 -0
  41. package/dist/lsp/server.d.ts +36 -0
  42. package/dist/lsp/server.js +281 -0
  43. package/dist/lsp/spec-location.d.ts +9 -0
  44. package/dist/lsp/spec-location.js +42 -0
  45. package/dist/lsp/typescript.d.ts +77 -0
  46. package/dist/lsp/typescript.js +242 -0
  47. package/dist/lsp/workspace.d.ts +56 -0
  48. package/dist/lsp/workspace.js +228 -0
  49. package/dist/optimistic.d.ts +3 -0
  50. package/dist/optimistic.js +15 -0
  51. package/dist/paginate.d.ts +3 -0
  52. package/dist/paginate.js +198 -0
  53. package/dist/persist.d.ts +3 -0
  54. package/dist/persist.js +55 -0
  55. package/dist/project.d.ts +42 -0
  56. package/dist/project.js +103 -0
  57. package/dist/runtime/app.d.ts +152 -0
  58. package/dist/runtime/app.js +1256 -0
  59. package/dist/runtime/config.d.ts +53 -0
  60. package/dist/runtime/config.js +118 -0
  61. package/dist/runtime/env.d.ts +18 -0
  62. package/dist/runtime/env.js +23 -0
  63. package/dist/runtime/helpers.d.ts +9 -0
  64. package/dist/runtime/helpers.js +55 -0
  65. package/dist/runtime/request.d.ts +23 -0
  66. package/dist/runtime/request.js +93 -0
  67. package/dist/runtime/security.d.ts +12 -0
  68. package/dist/runtime/security.js +34 -0
  69. package/dist/runtime/store.d.ts +98 -0
  70. package/dist/runtime/store.js +255 -0
  71. package/dist/sse.d.ts +7 -0
  72. package/dist/sse.js +209 -0
  73. package/dist/testing/index.d.ts +73 -0
  74. package/dist/testing/index.js +359 -0
  75. package/dist/vite.d.ts +11 -0
  76. package/dist/vite.js +96 -0
  77. package/dist/ws.d.ts +18 -0
  78. package/dist/ws.js +57 -0
  79. package/docs/diagnostics.md +283 -0
  80. package/html-custom-data.json +250 -0
  81. package/llms.txt +632 -0
  82. package/package.json +93 -4
@@ -0,0 +1,853 @@
1
+ // Expression analysis: parses one attribute value, resolves every identifier
2
+ // against the lexical scope, enforces the sandbox, extracts dependency paths
3
+ // (value expressions) and written paths (handlers), and emits a function.
4
+ import * as acorn from 'acorn';
5
+ import { didYouMean } from './diagnostics.js';
6
+ const EVERYWHERE_MAGIC = ['$el', '$url', '$now', '$locale', '$meta'];
7
+ export const ALL_MAGIC = ['$event', '$el', '$url', '$now', '$locale', '$result', '$error', '$meta'];
8
+ export const STORE_FNS = ['refresh', 'upsert', 'patch', 'remove', 'page', 'close', 'reset', 'emit'];
9
+ export const BUILTIN_HELPERS = ['date', 'time', 'datetime', 'relative', 'number', 'bytes'];
10
+ export const META_FIELDS = ['loading', 'error', 'loadedAt', 'pending', 'total', 'page', 'pages', 'from', 'to', 'hasPrev', 'hasNext'];
11
+ const PAGE_FIELDS = ['page', 'pages', 'from', 'to', 'hasPrev', 'hasNext'];
12
+ export const GLOBALS = new Set([
13
+ 'undefined', 'NaN', 'Infinity', 'isFinite', 'isNaN', 'parseFloat', 'parseInt',
14
+ 'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent',
15
+ 'Array', 'ArrayBuffer', 'BigInt', 'BigInt64Array', 'BigUint64Array', 'Boolean', 'DataView', 'Date',
16
+ 'Error', 'EvalError', 'AggregateError', 'Float32Array', 'Float64Array', 'Int8Array', 'Int16Array',
17
+ 'Int32Array', 'Intl', 'Iterator', 'JSON', 'Map', 'Math', 'Number', 'Object', 'Promise', 'RangeError',
18
+ 'ReferenceError', 'RegExp', 'Set', 'String', 'Symbol', 'SyntaxError', 'TypeError', 'URIError',
19
+ 'Uint8Array', 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', 'WeakSet',
20
+ 'URL', 'URLSearchParams', 'structuredClone',
21
+ ]);
22
+ const META_TOOLS = new Set(['eval', 'Function', 'Proxy', 'Reflect', 'globalThis', 'WeakRef', 'FinalizationRegistry', 'Atomics']);
23
+ const BROWSER = new Set([
24
+ 'window', 'document', 'fetch', 'localStorage', 'sessionStorage', 'navigator', 'location', 'history',
25
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'requestAnimationFrame', 'queueMicrotask',
26
+ 'XMLHttpRequest', 'WebSocket', 'EventSource', 'alert', 'confirm', 'prompt', 'console', 'self', 'top',
27
+ 'parent', 'frames', 'opener', 'indexedDB', 'crypto', 'performance', 'SharedArrayBuffer', 'importScripts',
28
+ 'customElements', 'Worker', 'Notification', 'caches',
29
+ ]);
30
+ const FORBIDDEN_KEYS = new Set(['constructor', '__proto__', 'prototype']);
31
+ const READ_ONLY = new Set([
32
+ 'includes', 'indexOf', 'lastIndexOf', 'map', 'filter', 'slice', 'find', 'findIndex', 'findLast',
33
+ 'findLastIndex', 'some', 'every', 'reduce', 'reduceRight', 'join', 'concat', 'flat', 'flatMap', 'at',
34
+ 'entries', 'keys', 'values', 'forEach', 'toString', 'toLocaleString', 'toSorted', 'toReversed',
35
+ 'toSpliced', 'with', 'has', 'get', 'startsWith', 'endsWith', 'trim', 'trimStart', 'trimEnd',
36
+ 'toLowerCase', 'toUpperCase', 'toLocaleLowerCase', 'toLocaleUpperCase', 'split', 'substring',
37
+ 'charAt', 'charCodeAt', 'codePointAt', 'padStart', 'padEnd', 'repeat', 'replace', 'replaceAll',
38
+ 'match', 'matchAll', 'search', 'localeCompare', 'normalize', 'toFixed', 'toPrecision', 'valueOf',
39
+ 'getTime', 'toISOString', 'toJSON', 'getFullYear', 'getMonth', 'getDate', 'getDay', 'getHours',
40
+ 'getMinutes', 'getSeconds', 'test', 'exec',
41
+ ]);
42
+ const MUTATORS = new Set(['push', 'pop', 'splice', 'sort', 'reverse', 'shift', 'unshift', 'fill', 'copyWithin', 'add', 'delete', 'clear', 'set']);
43
+ /** callbacks of these methods receive elements of the receiver as first parameter */
44
+ const ELEMENT_CALLBACKS = new Set(['forEach', 'map', 'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'some', 'every', 'flatMap', 'toSorted', 'sort']);
45
+ export function analyze(src, scope, page, opts) {
46
+ const edits = [];
47
+ const deps = new Set();
48
+ const writes = new Set();
49
+ let usesEl = false;
50
+ let usesIndex = false;
51
+ let ok = true;
52
+ const payloadNames = new Set();
53
+ const payloadStates = new Set();
54
+ const mode = opts.mode;
55
+ const magicAllowed = new Set([...EVERYWHERE_MAGIC, ...(opts.magic ?? [])]);
56
+ const report = (code, message, offset, hint) => {
57
+ if (code < 'SN500')
58
+ ok = false;
59
+ opts.report(code, message, offset, hint);
60
+ };
61
+ // ---- parsing
62
+ let ast = null;
63
+ let body = [];
64
+ const parseOpts = { ecmaVersion: 'latest', sourceType: 'module', allowAwaitOutsideFunction: false };
65
+ try {
66
+ if (mode === 'value' || opts.object) {
67
+ ast = acorn.parseExpressionAt(src, 0, parseOpts);
68
+ // without preserveParens, `(a ? b : c)` ends inside its parentheses; find the real end
69
+ const end = acorn.parseExpressionAt(src, 0, { ...parseOpts, preserveParens: true }).end;
70
+ const rest = src.slice(end).trim();
71
+ if (rest) {
72
+ const at = src.indexOf(rest, end);
73
+ report('SN401', `unexpected \`${rest.slice(0, 20)}\` after the expression`, at);
74
+ return fail();
75
+ }
76
+ body = [ast];
77
+ }
78
+ else {
79
+ const prog = acorn.parse(src, parseOpts);
80
+ ast = prog;
81
+ for (const st of prog.body) {
82
+ if (st.type === 'EmptyStatement')
83
+ continue;
84
+ if (st.type !== 'ExpressionStatement') {
85
+ const what = st.type.endsWith('Declaration') ? 'declarations' : st.type.replace(/Statement$/, '').toLowerCase() + ' statements';
86
+ report('SN203', `handlers cannot contain ${what}; use expressions separated by \`;\``, st.start);
87
+ continue;
88
+ }
89
+ body.push(st.expression);
90
+ }
91
+ }
92
+ }
93
+ catch (e) {
94
+ const err = e;
95
+ report('SN401', err.message.replace(/ \(\d+:\d+\)$/, ''), err.pos ?? 0);
96
+ return fail();
97
+ }
98
+ function fail() {
99
+ return { ok: false, code: '() => undefined', deps: [], writes: [], usesEl: false, usesIndex: false, path: null, resource: null, ast: null };
100
+ }
101
+ // ---- resolution
102
+ function resolve(name, locals, offset, member) {
103
+ const local = locals.get(name);
104
+ if (local)
105
+ return { kind: 'local', info: local };
106
+ if (name.startsWith('$')) {
107
+ if (ALL_MAGIC.includes(name)) {
108
+ if (!magicAllowed.has(name)) {
109
+ report('SN201', `\`${name}\` is not available here`, offset);
110
+ return null;
111
+ }
112
+ if (name === '$el')
113
+ usesEl = true;
114
+ if (name === '$now')
115
+ page.usesNow = true;
116
+ return { kind: 'magic' };
117
+ }
118
+ report('SN201', `\`${name}\` is not defined.${didYouMean(name, ALL_MAGIC)}`, offset);
119
+ return null;
120
+ }
121
+ if (opts.payload && opts.payloadState) {
122
+ for (let i = scope.states.length - 1; i >= 0; i--) {
123
+ if (scope.states[i].names.has(name)) {
124
+ payloadStates.add(name);
125
+ return { kind: 'state', sid: scope.states[i].sid };
126
+ }
127
+ }
128
+ }
129
+ if (!opts.payload) {
130
+ for (let i = scope.loops.length - 1; i >= 0; i--) {
131
+ if (scope.loops[i].name === name)
132
+ return { kind: 'loop', entry: scope.loops[i] };
133
+ }
134
+ for (let i = scope.states.length - 1; i >= 0; i--) {
135
+ if (scope.states[i].names.has(name))
136
+ return { kind: 'state', sid: scope.states[i].sid };
137
+ }
138
+ const r = page.resources.get(name);
139
+ if (r) {
140
+ r.used = true;
141
+ return { kind: 'resource' };
142
+ }
143
+ }
144
+ if (STORE_FNS.includes(name) && !opts.payload)
145
+ return { kind: 'store' };
146
+ if (page.namespaces.has(name))
147
+ return { kind: 'ns' };
148
+ if (page.helpers.has(name) || BUILTIN_HELPERS.includes(name))
149
+ return { kind: 'helper' };
150
+ if (GLOBALS.has(name))
151
+ return { kind: 'global' };
152
+ if (META_TOOLS.has(name)) {
153
+ report('SN202', `\`${name}\` is not allowed in expressions`, offset);
154
+ return null;
155
+ }
156
+ if (opts.payload) {
157
+ payloadNames.add(name);
158
+ return { kind: 'payload' };
159
+ }
160
+ if (page.actions?.has(name)) {
161
+ report('SN203', `\`${name}\` is an action: actions run through \`data-sn-call\` or \`<sn-resource call>\`, not inside expressions`, offset);
162
+ return null;
163
+ }
164
+ if (BROWSER.has(name)) {
165
+ report('SN202', `\`${name}\` is a browser API; expressions may use JavaScript, not the browser. Use an island`, offset);
166
+ return null;
167
+ }
168
+ if (opts.payload) {
169
+ payloadNames.add(name);
170
+ return { kind: 'payload' };
171
+ }
172
+ const candidates = [
173
+ ...scope.loops.map((l) => l.name), ...scope.states.flatMap((s) => [...s.names]),
174
+ ...page.resources.keys(), ...page.helpers, ...page.namespaces.keys(), ...STORE_FNS, ...BUILTIN_HELPERS,
175
+ ];
176
+ report('SN201', `\`${name}\` is not defined here.${didYouMean(name, candidates)}`, offset);
177
+ void member;
178
+ return null;
179
+ }
180
+ function emitName(name, res, sid) {
181
+ switch (res.kind) {
182
+ case 'local':
183
+ case 'global':
184
+ case 'helper': return name;
185
+ case 'loop': return `$.${name}`;
186
+ case 'state': return `$.$s${sid}.${name}`;
187
+ case 'resource': return `$.$r.${name}`;
188
+ case 'magic': return `$.${name}`;
189
+ case 'store': return `$.$f.${name}`;
190
+ case 'payload': return `$.$p.${name}`;
191
+ case 'ns': return name;
192
+ }
193
+ }
194
+ function basePath(name, res) {
195
+ switch (res.kind) {
196
+ case 'state': return { segs: [`$s${res.sid}`, name], resource: null };
197
+ case 'resource': return { segs: ['$r', name], resource: name };
198
+ case 'magic':
199
+ return name === '$el' || name === '$event' || name === '$result' || name === '$error'
200
+ ? { segs: null, resource: null } : { segs: [name], resource: null };
201
+ case 'loop':
202
+ return { segs: res.entry.alias ? [...res.entry.alias] : null, resource: res.entry.resource };
203
+ case 'local':
204
+ return { segs: res.info.alias ? [...res.info.alias] : null, resource: res.info.resource };
205
+ default: return { segs: null, resource: null };
206
+ }
207
+ }
208
+ /** Static path of an Identifier/MemberExpression chain; null if the leftmost object is not an identifier. */
209
+ function chainPath(n, locals) {
210
+ const props = [];
211
+ let cur = n;
212
+ while (cur.type === 'MemberExpression') {
213
+ const m = cur;
214
+ if (!m.computed && m.property.type === 'Identifier')
215
+ props.unshift(m.property.name);
216
+ else if (m.computed && m.property.type === 'Literal')
217
+ props.unshift(String(m.property.value));
218
+ else if (m.computed && m.property.type === 'Identifier') {
219
+ const pn = m.property.name;
220
+ const loop = !locals.has(pn) && !opts.payload ? [...scope.loops].reverse().find((l) => l.name === pn) : undefined;
221
+ props.unshift(loop && loop.kind === 'index' ? `{${pn}}` : '*');
222
+ }
223
+ else
224
+ props.unshift('*');
225
+ cur = m.object;
226
+ }
227
+ if (cur.type === 'ChainExpression')
228
+ return chainPath(cur.expression, locals);
229
+ if (cur.type !== 'Identifier')
230
+ return null;
231
+ const name = cur.name;
232
+ const res = resolveQuiet(name, locals);
233
+ if (!res)
234
+ return null;
235
+ const base = basePath(name, res);
236
+ const segs = base.segs ? [...base.segs, ...props] : null;
237
+ // `list[i]` where `i` indexes that same list is the row's own element: use its row placeholder,
238
+ // which the runtime may skip on structure changes (reconcile handles index moves)
239
+ segs?.forEach((seg, k) => {
240
+ const loop = seg[0] == '{' ? [...scope.loops].reverse().find((l) => l.kind == 'index' && `{${l.name}}` == seg) : undefined;
241
+ if (loop?.of && loop.of.length == k + 1 && loop.of.slice(0, k).join('.') == segs.slice(0, k).join('.'))
242
+ segs[k] = loop.of[k];
243
+ });
244
+ return { root: name, res, segs, resource: base.resource };
245
+ }
246
+ /** Resolution without diagnostics, for chains already resolved by the walker. */
247
+ function resolveQuiet(name, locals) {
248
+ const saved = opts.report;
249
+ const savedOk = ok;
250
+ const savedEl = usesEl;
251
+ opts.report = () => { };
252
+ try {
253
+ return resolve(name, locals, 0);
254
+ }
255
+ finally {
256
+ opts.report = saved;
257
+ ok = savedOk;
258
+ usesEl = savedEl || usesEl;
259
+ }
260
+ }
261
+ function addDep(segs) {
262
+ if (!segs)
263
+ return;
264
+ // `$meta.<name>.<field>`: validate resource and field
265
+ if (segs[0] === '$meta') {
266
+ if (segs.length < 2 || segs[1] === '*') {
267
+ deps.add('$meta');
268
+ return;
269
+ }
270
+ deps.add(segs.slice(0, 3).join('.'));
271
+ return;
272
+ }
273
+ deps.add(segs.join('.'));
274
+ }
275
+ function checkMeta(n, locals) {
276
+ const cp = chainPath(n, locals);
277
+ if (!cp || cp.root !== '$meta' || cp.res.kind !== 'magic' || !cp.segs)
278
+ return;
279
+ const [, rname, field] = cp.segs;
280
+ if (rname && rname !== '*') {
281
+ const r = page.resources.get(rname);
282
+ if (!r) {
283
+ report('SN301', `resource \`${rname}\` is not declared on this page.${didYouMean(rname, page.resources.keys())}`, n.start);
284
+ return;
285
+ }
286
+ r.used = true;
287
+ if (field && field !== '*') {
288
+ if (!META_FIELDS.includes(field)) {
289
+ report('SN303', `\`$meta.${rname}.${field}\` is not a status field.${didYouMean(field, META_FIELDS)}`, n.start);
290
+ }
291
+ else if (PAGE_FIELDS.includes(field) && !r.paginate) {
292
+ report('SN303', `\`$meta.${rname}.${field}\` is a pagination field, but \`${rname}\` has no \`paginate\``, n.start);
293
+ }
294
+ else if (field === 'page' && r.paginate !== 'offset' && !r.jumps) {
295
+ report('SN303', `\`$meta.${rname}.page\` exists only with \`paginate="offset"\`, or \`paginate="keyset"\` with \`offset-param\``, n.start);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ // ---- walking
301
+ function ident(n, locals, shorthand = false) {
302
+ const res = resolve(n.name, locals, n.start);
303
+ if (!res)
304
+ return;
305
+ if (res.kind === 'ns') {
306
+ report('SN206', `namespace \`${n.name}\` must be used as \`${n.name}.<member>()\``, n.start);
307
+ return;
308
+ }
309
+ if (res.kind === 'helper' && BUILTIN_HELPERS.includes(n.name) && !page.helpers.has(n.name))
310
+ page.usedHelpers.add(n.name);
311
+ else if (res.kind === 'helper')
312
+ page.usedHelpers.add(n.name);
313
+ let sid;
314
+ if (res.kind === 'state')
315
+ sid = res.sid;
316
+ if (res.kind === 'loop' && res.entry.kind === 'index')
317
+ usesIndex = true;
318
+ const text = emitName(n.name, res, sid);
319
+ if (text !== n.name || shorthand)
320
+ edits.push({ start: n.start, end: n.end, text: shorthand ? `${n.name}: ${text}` : text });
321
+ }
322
+ function declareParams(params, locals, info) {
323
+ const next = new Map(locals);
324
+ params.forEach((p, i) => {
325
+ const names = [];
326
+ collectPattern(p, names, next);
327
+ for (const nm of names)
328
+ next.set(nm, i === 0 && p.type === 'Identifier' && info ? info : { alias: null, resource: info?.resource ?? null });
329
+ });
330
+ return next;
331
+ }
332
+ function collectPattern(p, out, locals) {
333
+ switch (p.type) {
334
+ case 'Identifier':
335
+ out.push(p.name);
336
+ break;
337
+ case 'ObjectPattern':
338
+ for (const prop of p.properties) {
339
+ if (prop.type === 'RestElement')
340
+ collectPattern(prop.argument, out, locals);
341
+ else {
342
+ if (prop.computed)
343
+ visit(prop.key, locals);
344
+ collectPattern(prop.value, out, locals);
345
+ }
346
+ }
347
+ break;
348
+ case 'ArrayPattern':
349
+ for (const el of p.elements)
350
+ if (el)
351
+ collectPattern(el, out, locals);
352
+ break;
353
+ case 'RestElement':
354
+ collectPattern(p.argument, out, locals);
355
+ break;
356
+ case 'AssignmentPattern':
357
+ collectPattern(p.left, out, locals);
358
+ visit(p.right, locals);
359
+ break;
360
+ default: report('SN401', `unsupported parameter pattern`, p.start);
361
+ }
362
+ }
363
+ function visitFunction(fn, locals, elementInfo) {
364
+ if (fn.async)
365
+ report('SN203', '`async` functions are not allowed in expressions', fn.start);
366
+ const inner = declareParams(fn.params, locals, elementInfo);
367
+ visit(fn.body, inner);
368
+ }
369
+ function visitChain(n, locals, isCallee) {
370
+ // dependency of the whole chain (minus the method name when it is called)
371
+ if (mode !== 'optimistic' || true) {
372
+ const cp = chainPath(n, locals);
373
+ if (cp && cp.segs) {
374
+ const segs = isCallee && n.type === 'MemberExpression' ? cp.segs.slice(0, -1) : cp.segs;
375
+ if (mode === 'value' || isCallee || true)
376
+ addDep(segs.length ? segs : null);
377
+ }
378
+ }
379
+ visitMemberParts(n, locals);
380
+ }
381
+ function visitMemberParts(n, locals) {
382
+ if (n.type === 'Identifier')
383
+ return ident(n, locals);
384
+ if (n.type === 'ChainExpression')
385
+ return visitMemberParts(n.expression, locals);
386
+ if (n.type !== 'MemberExpression')
387
+ return visit(n, locals);
388
+ const m = n;
389
+ if (m.object.type === 'Identifier' && page.namespaces.has(m.object.name) && !locals.has(m.object.name)) {
390
+ nsMember(m, locals);
391
+ return;
392
+ }
393
+ if (m.object.type === 'Identifier' && m.object.name === '$meta')
394
+ checkMeta(m, locals);
395
+ else if (m.object.type === 'MemberExpression') {
396
+ const root = leftmost(m);
397
+ if (root?.type === 'Identifier' && root.name === '$meta')
398
+ checkMeta(m, locals);
399
+ }
400
+ if (m.object.type === 'MemberExpression' || m.object.type === 'Identifier' || m.object.type === 'ChainExpression')
401
+ visitMemberParts(m.object, locals);
402
+ else
403
+ visit(m.object, locals);
404
+ if (!m.computed) {
405
+ const pn = m.property.name;
406
+ if (FORBIDDEN_KEYS.has(pn))
407
+ report('SN207', `access to \`.${pn}\` is not allowed`, m.property.start);
408
+ }
409
+ else if (m.property.type === 'Literal') {
410
+ if (FORBIDDEN_KEYS.has(String(m.property.value)))
411
+ report('SN207', `access to \`[${m.property.raw}]\` is not allowed`, m.property.start);
412
+ }
413
+ else {
414
+ edits.push({ start: m.property.start, end: m.property.start, text: '$k(' });
415
+ edits.push({ start: m.property.end, end: m.property.end, text: ')' });
416
+ visit(m.property, locals);
417
+ }
418
+ }
419
+ function leftmost(n) {
420
+ let cur = n;
421
+ while (cur.type === 'MemberExpression' || cur.type === 'CallExpression' || cur.type === 'ChainExpression') {
422
+ if (cur.type === 'MemberExpression')
423
+ cur = cur.object;
424
+ else if (cur.type === 'CallExpression')
425
+ cur = cur.callee;
426
+ else
427
+ cur = cur.expression;
428
+ }
429
+ return cur;
430
+ }
431
+ function nsMember(m, _locals) {
432
+ const ns = m.object.name;
433
+ if (m.computed) {
434
+ report('SN206', `namespace members must be named literally; \`${ns}[…]\` is refused. Put a dynamic choice in a helper`, m.start);
435
+ return;
436
+ }
437
+ const member = m.property.name;
438
+ const members = page.namespaces.get(ns);
439
+ if (!members.has(member)) {
440
+ report('SN206', `\`${ns}.${member}\` is not exported by namespace \`${ns}\`.${didYouMean(member, members)}`, m.start);
441
+ return;
442
+ }
443
+ if (!page.usedNs.has(ns))
444
+ page.usedNs.set(ns, new Set());
445
+ page.usedNs.get(ns).add(member);
446
+ addDep(['$locale']);
447
+ edits.push({ start: m.start, end: m.end, text: `${ns}$${member}` });
448
+ }
449
+ /**
450
+ * A write through a callback parameter that stands for "any element" (`data.forEach(row => row.x = 1)`)
451
+ * records the element at runtime: the expression is wrapped in `$.$m(row, …)` and its `*` becomes `~`,
452
+ * so only rows holding a marked element re-evaluate.
453
+ */
454
+ function track(cp, segs, expr) {
455
+ const alias = cp.res.kind === 'local' ? cp.res.info.alias : null;
456
+ if (!alias || alias[alias.length - 1] !== '*' || segs[alias.length - 1] !== '*')
457
+ return segs;
458
+ edits.push({ start: expr.start, end: expr.start, text: `$.$m(${cp.root}, ` }, { start: expr.end, end: expr.end, text: ')' });
459
+ return segs.map((x, i) => (i === alias.length - 1 ? '~' : x));
460
+ }
461
+ function writePath(target, locals, at, expr) {
462
+ if (target.type === 'Identifier' && target.name === '$locale') {
463
+ writes.add('$locale');
464
+ return;
465
+ }
466
+ if (target.type !== 'Identifier' && target.type !== 'MemberExpression') {
467
+ report('SN203', 'only state variables and their properties can be assigned', at);
468
+ return;
469
+ }
470
+ const cp = chainPath(target, locals);
471
+ if (!cp) {
472
+ report('SN203', 'only state variables and their properties can be assigned', at);
473
+ return;
474
+ }
475
+ if (cp.resource) {
476
+ report('SN205', `\`${src.slice(target.start, target.end)}\` mutates a resource. Use \`patch('${cp.resource}', …, {…})\``, at, 'resources are read-only in expressions; copy with structuredClone() to edit');
477
+ return;
478
+ }
479
+ if (cp.res.kind === 'magic' && cp.root === '$url') {
480
+ if (!cp.segs || cp.segs.length !== 2) {
481
+ report('SN203', 'assign a parameter: `$url.<param> = …`', at);
482
+ return;
483
+ }
484
+ if (page.pathParams.has(cp.segs[1])) {
485
+ report('SN203', `\`$url.${cp.segs[1]}\` is a path parameter (\`data-sn-path\`) and read-only; link to the other URL instead`, at);
486
+ return;
487
+ }
488
+ writes.add(cp.segs.join('.'));
489
+ return;
490
+ }
491
+ if (cp.res.kind === 'state' || ((cp.res.kind === 'loop' || cp.res.kind === 'local') && cp.segs)) {
492
+ if (cp.res.kind === 'loop' && cp.segs.length === (cp.res.entry.alias?.length ?? 0)) {
493
+ report('SN203', `loop variable \`${cp.root}\` cannot be reassigned`, at);
494
+ return;
495
+ }
496
+ writes.add(track(cp, cp.segs, expr).join('.'));
497
+ return;
498
+ }
499
+ report('SN203', `\`${src.slice(target.start, target.end)}\` cannot be assigned; only state variables, \`$url.<param>\` and \`$locale\` can`, at);
500
+ }
501
+ function visitCall(n, locals) {
502
+ const callee = (n.callee.type === 'ChainExpression' ? n.callee.expression : n.callee);
503
+ let elementInfo = null;
504
+ if (callee.type === 'MemberExpression') {
505
+ const m = callee;
506
+ const method = !m.computed && m.property.type === 'Identifier' ? m.property.name : null;
507
+ const isNs = m.object.type === 'Identifier' && page.namespaces.has(m.object.name) && !locals.has(m.object.name);
508
+ if (!isNs) {
509
+ const recv = chainPath(m.object, locals);
510
+ if (method && recv) {
511
+ if (ELEMENT_CALLBACKS.has(method)) {
512
+ elementInfo = { alias: recv.segs ? [...recv.segs, '*'] : null, resource: recv.resource };
513
+ }
514
+ if (!READ_ONLY.has(method)) {
515
+ if (recv.resource && MUTATORS.has(method)) {
516
+ report('SN205', `\`${src.slice(m.start, m.end)}()\` mutates a resource. Use \`patch('${recv.resource}', …)\` or copy with structuredClone()`, n.start);
517
+ }
518
+ else if (mode === 'value' && MUTATORS.has(method) && recv.segs && recv.res.kind === 'state') {
519
+ report('SN203', `value expressions cannot change state; move \`${method}()\` to \`data-sn-on-*\``, n.start);
520
+ }
521
+ else if (mode === 'handler' && recv.segs && recv.res.kind !== 'magic' && !recv.resource) {
522
+ // a known mutator changes the list structure (`#`), not the elements; unknown methods may change anything
523
+ writes.add(track(recv, recv.segs, n).join('.') + (MUTATORS.has(method) ? '.#' : ''));
524
+ }
525
+ else if (mode === 'handler' && recv.segs && recv.root === '$url') {
526
+ writes.add(recv.segs.slice(0, 2).join('.'));
527
+ }
528
+ }
529
+ }
530
+ // time and randomness
531
+ if (mode === 'value' && m.object.type === 'Identifier' && !m.computed) {
532
+ const o = m.object.name, p = m.property.name;
533
+ if ((o === 'Date' && p === 'now') || (o === 'Math' && p === 'random')) {
534
+ report('SN502', `\`${o}.${p}()\` in a binding never updates. Use \`$now\``, n.start);
535
+ }
536
+ }
537
+ }
538
+ visitChain(callee, locals, true);
539
+ }
540
+ else if (callee.type === 'Identifier') {
541
+ const res = resolveQuiet(callee.name, locals);
542
+ ident(callee, locals);
543
+ if (res?.kind === 'store')
544
+ storeCall(callee.name, n);
545
+ else if (res?.kind === 'helper') {
546
+ if (BUILTIN_HELPERS.includes(callee.name))
547
+ addDep(['$locale']);
548
+ else if (mode === 'handler') {
549
+ for (const a of n.arguments) {
550
+ const cp = a.type === 'Identifier' || a.type === 'MemberExpression' ? chainPath(a, locals) : null;
551
+ if (cp?.segs && cp.res.kind === 'state')
552
+ writes.add(cp.segs.slice(0, 2).join('.'));
553
+ }
554
+ }
555
+ }
556
+ }
557
+ else {
558
+ visit(callee, locals);
559
+ }
560
+ for (const a of n.arguments) {
561
+ if (a.type === 'ArrowFunctionExpression')
562
+ visitFunction(a, locals, elementInfo);
563
+ else
564
+ visit(a, locals);
565
+ }
566
+ }
567
+ function storeCall(name, n) {
568
+ if (mode === 'value') {
569
+ report('SN203', `value expressions cannot call \`${name}()\`; move it to a handler`, n.start);
570
+ return;
571
+ }
572
+ const first = n.arguments[0];
573
+ if (['refresh', 'upsert', 'patch', 'remove', 'page'].includes(name)) {
574
+ if (!first || first.type !== 'Literal' || typeof first.value !== 'string') {
575
+ report('SN301', `\`${name}()\` takes the resource name as a string literal`, n.start);
576
+ return;
577
+ }
578
+ const r = page.resources.get(first.value);
579
+ if (!r) {
580
+ report('SN301', `resource \`${first.value}\` is not declared on this page.${didYouMean(first.value, page.resources.keys())}`, first.start);
581
+ return;
582
+ }
583
+ if (name === 'page') {
584
+ if (!r.paginate)
585
+ report('SN303', `\`${first.value}\` has no \`paginate\`; \`page()\` needs a paginated resource`, n.start);
586
+ const target = n.arguments[1];
587
+ const named = target?.type === 'Literal' && typeof target.value === 'string' ? target.value : null;
588
+ if (r.paginate && r.paginate !== 'offset' && !named && !(r.paginate === 'keyset' && r.jumps)) {
589
+ report('SN304', r.paginate === 'keyset'
590
+ ? 'keyset pagination cannot jump to a page number. Add `offset-param` if the server also accepts an offset, or use `paginate="offset"`'
591
+ : 'keyset pagination cannot jump to a page number. Use `paginate="offset"`', target?.start ?? n.start);
592
+ }
593
+ else if (r.paginate === 'append' && named && named !== 'next') {
594
+ report('SN304', '`append` pagination only supports `page(…, \'next\')`', target.start);
595
+ }
596
+ else if (named && !['first', 'prev', 'next', 'last'].includes(named)) {
597
+ report('SN401', `\`page()\` targets are 'first', 'prev', 'next', 'last' or a page number`, target.start);
598
+ }
599
+ }
600
+ }
601
+ }
602
+ function visit(n, locals) {
603
+ switch (n.type) {
604
+ case 'Identifier': {
605
+ const res = resolveQuiet(n.name, locals);
606
+ if (res) {
607
+ const b = basePath(n.name, res);
608
+ addDep(b.segs);
609
+ }
610
+ ident(n, locals);
611
+ return;
612
+ }
613
+ case 'MemberExpression':
614
+ case 'ChainExpression':
615
+ visitChain(n, locals, false);
616
+ return;
617
+ case 'CallExpression':
618
+ visitCall(n, locals);
619
+ return;
620
+ case 'NewExpression':
621
+ if (mode === 'value' && n.callee.type === 'Identifier' && n.callee.name === 'Date' && n.arguments.length === 0) {
622
+ report('SN502', '`new Date()` in a binding never updates. Use `$now`', n.start);
623
+ }
624
+ visit(n.callee, locals);
625
+ for (const a of n.arguments)
626
+ visit(a, locals);
627
+ return;
628
+ case 'ArrowFunctionExpression':
629
+ visitFunction(n, locals, null);
630
+ return;
631
+ case 'FunctionExpression':
632
+ report('SN203', 'use an arrow function instead of `function`', n.start);
633
+ return;
634
+ case 'ClassExpression':
635
+ report('SN203', 'classes are not allowed in expressions', n.start);
636
+ return;
637
+ case 'AssignmentExpression': {
638
+ if (mode !== 'handler') {
639
+ report('SN203', mode === 'optimistic' ? '`data-sn-optimistic` may only call `patch`, `upsert`, `remove`' : 'value expressions cannot assign; move this to `data-sn-on-*`', n.start);
640
+ return;
641
+ }
642
+ if (n.left.type === 'ObjectPattern' || n.left.type === 'ArrayPattern') {
643
+ report('SN203', 'destructuring assignment is not supported in handlers', n.start);
644
+ return;
645
+ }
646
+ writePath(n.left, locals, n.start, n);
647
+ visitMemberParts(n.left, locals);
648
+ visit(n.right, locals);
649
+ return;
650
+ }
651
+ case 'UpdateExpression':
652
+ if (mode !== 'handler') {
653
+ report('SN203', 'value expressions cannot use `++`/`--`; move this to `data-sn-on-*`', n.start);
654
+ return;
655
+ }
656
+ writePath(n.argument, locals, n.start, n);
657
+ visitMemberParts(n.argument, locals);
658
+ return;
659
+ case 'UnaryExpression':
660
+ if (n.operator === 'delete') {
661
+ if (mode !== 'handler') {
662
+ report('SN203', 'value expressions cannot use `delete`', n.start);
663
+ return;
664
+ }
665
+ writePath(n.argument, locals, n.start, n);
666
+ visitMemberParts(n.argument, locals);
667
+ return;
668
+ }
669
+ visit(n.argument, locals);
670
+ return;
671
+ case 'AwaitExpression':
672
+ report('SN203', '`await` is not allowed in expressions', n.start);
673
+ return;
674
+ case 'ImportExpression':
675
+ report('SN203', '`import()` is not allowed in expressions', n.start);
676
+ return;
677
+ case 'YieldExpression':
678
+ report('SN203', '`yield` is not allowed in expressions', n.start);
679
+ return;
680
+ case 'ThisExpression':
681
+ report('SN202', '`this` is not available in expressions; use `$el`', n.start);
682
+ return;
683
+ case 'MetaProperty':
684
+ report('SN202', `\`${src.slice(n.start, n.end)}\` is not available in expressions`, n.start);
685
+ return;
686
+ case 'ObjectExpression':
687
+ for (const p of n.properties) {
688
+ if (p.type === 'SpreadElement') {
689
+ visit(p.argument, locals);
690
+ continue;
691
+ }
692
+ if (p.computed)
693
+ visit(p.key, locals);
694
+ if (p.shorthand && p.value.type === 'Identifier') {
695
+ const res = resolveQuiet(p.value.name, locals);
696
+ if (res)
697
+ addDep(basePath(p.value.name, res).segs);
698
+ ident(p.value, locals, true);
699
+ }
700
+ else if (p.kind !== 'init' || p.method) {
701
+ report('SN203', 'methods and accessors are not allowed in object literals', p.start);
702
+ }
703
+ else
704
+ visit(p.value, locals);
705
+ }
706
+ return;
707
+ case 'TaggedTemplateExpression':
708
+ visit(n.tag, locals);
709
+ visit(n.quasi, locals);
710
+ return;
711
+ case 'SequenceExpression':
712
+ for (const e of n.expressions)
713
+ visit(e, locals);
714
+ return;
715
+ default:
716
+ for (const key of Object.keys(n)) {
717
+ if (key === 'type' || key === 'start' || key === 'end' || key === 'loc' || key === 'range')
718
+ continue;
719
+ const v = n[key];
720
+ if (Array.isArray(v)) {
721
+ for (const c of v)
722
+ if (c && typeof c === 'object' && 'type' in c)
723
+ visit(c, locals);
724
+ }
725
+ else if (v && typeof v === 'object' && 'type' in v)
726
+ visit(v, locals);
727
+ }
728
+ }
729
+ }
730
+ // ---- optimistic: only patch/upsert/remove calls
731
+ function checkOptimistic(e) {
732
+ if (e.type !== 'CallExpression' || e.callee.type !== 'Identifier' || !['patch', 'upsert', 'remove'].includes(e.callee.name)) {
733
+ report('SN204', '`data-sn-optimistic` may only call `patch`, `upsert`, `remove`', e.start, 'local state and side effects cannot be rolled back; put them in `data-sn-then`');
734
+ return false;
735
+ }
736
+ return true;
737
+ }
738
+ // ---- objects
739
+ let entries;
740
+ const root = new Map();
741
+ if (opts.object) {
742
+ if (ast.type !== 'ObjectExpression') {
743
+ report('SN401', 'expected an object literal `{ … }`', 0);
744
+ return fail();
745
+ }
746
+ entries = [];
747
+ for (const p of ast.properties) {
748
+ if (p.type === 'SpreadElement') {
749
+ report(opts.object === 'state' ? 'SN401' : 'SN207', 'spread (`...`) is not allowed here; list each key literally', p.start);
750
+ continue;
751
+ }
752
+ let key;
753
+ if (!p.computed && p.key.type === 'Identifier')
754
+ key = p.key.name;
755
+ else if (p.key.type === 'Literal' && !p.computed)
756
+ key = String(p.key.value);
757
+ else if (opts.object === 'any-keys')
758
+ key = '';
759
+ else {
760
+ report(opts.object === 'state' ? 'SN401' : 'SN207', 'keys must be literal; computed keys are not allowed here', p.start);
761
+ continue;
762
+ }
763
+ if (p.kind !== 'init' || p.method) {
764
+ report('SN203', 'methods and accessors are not allowed in object literals', p.start);
765
+ continue;
766
+ }
767
+ const depsBefore = new Set(deps);
768
+ deps.clear();
769
+ if (p.computed)
770
+ visit(p.key, root);
771
+ if (p.shorthand && p.value.type === 'Identifier') {
772
+ const res = resolveQuiet(p.value.name, root);
773
+ if (res)
774
+ addDep(basePath(p.value.name, res).segs);
775
+ ident(p.value, root, true);
776
+ }
777
+ else
778
+ visit(p.value, root);
779
+ const own = [...deps];
780
+ for (const d of depsBefore)
781
+ deps.add(d);
782
+ entries.push({ key, code: '', deps: own, offset: p.start, literal: p.value.type === 'Literal' });
783
+ }
784
+ }
785
+ else {
786
+ for (const e of body) {
787
+ if (mode === 'optimistic' && !checkOptimistic(e))
788
+ continue;
789
+ if (mode === 'optimistic') {
790
+ // arguments are read like values; only the call itself is allowed
791
+ const c = e;
792
+ ident(c.callee, root);
793
+ storeCall(c.callee.name, c);
794
+ for (const a of c.arguments)
795
+ visit(a, root);
796
+ }
797
+ else
798
+ visit(e, root);
799
+ }
800
+ }
801
+ // ---- codegen
802
+ const out = applyEdits(src, edits);
803
+ let code;
804
+ if (mode === 'value' || opts.object)
805
+ code = `($) => (${out})`;
806
+ else
807
+ code = `($) => {${out}\n}`;
808
+ if (entries && opts.object) {
809
+ // per-entry code for object literals, so bindings can write key by key
810
+ const slice = (start, end) => applyEdits(src.slice(start, end), edits.filter((e) => e.start >= start && e.end <= end).map((e) => ({ ...e, start: e.start - start, end: e.end - start })));
811
+ let i = 0;
812
+ for (const p of ast.properties) {
813
+ if (p.type === 'SpreadElement')
814
+ continue;
815
+ const entry = entries[i];
816
+ if (!entry || entry.offset !== p.start)
817
+ continue;
818
+ let value = slice(p.value.start, p.value.end);
819
+ if (p.shorthand)
820
+ value = value.slice(value.indexOf(': ') + 2);
821
+ entry.code = p.computed ? `($) => [${slice(p.key.start, p.key.end)}, ${value}]` : `($) => (${value})`;
822
+ i++;
823
+ }
824
+ }
825
+ let path = null;
826
+ let resource = null;
827
+ if (body.length === 1 && !opts.object) {
828
+ const e = body[0];
829
+ if (e.type === 'Identifier' || e.type === 'MemberExpression') {
830
+ const cp = chainPath(e, root);
831
+ if (cp) {
832
+ path = cp.segs;
833
+ resource = cp.resource;
834
+ }
835
+ }
836
+ if (!resource) {
837
+ const l = leftmost(e);
838
+ if (l?.type === 'Identifier') {
839
+ const res = resolveQuiet(l.name, root);
840
+ if (res)
841
+ resource = basePath(l.name, res).resource;
842
+ }
843
+ }
844
+ }
845
+ return { ok, code, deps: [...deps].sort(), writes: [...writes].sort(), usesEl, usesIndex, entries, path, resource, ast, ...(opts.payload ? { payloadNames: [...payloadNames].sort(), payloadStates: [...payloadStates].sort() } : {}) };
846
+ }
847
+ export function applyEdits(src, edits) {
848
+ const sorted = [...edits].sort((a, b) => b.start - a.start || (b.end - b.start) - (a.end - a.start));
849
+ let out = src;
850
+ for (const e of sorted)
851
+ out = out.slice(0, e.start) + e.text + out.slice(e.end);
852
+ return out;
853
+ }