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