@tiaforge-ui/sinter 0.1.0 → 0.3.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/README.md +39 -12
- package/dist/check/index.d.ts +4 -1
- package/dist/check/index.js +9 -8
- package/dist/check/shadow.d.ts +3 -1
- package/dist/check/shadow.js +53 -5
- package/dist/cli.js +2 -2
- package/dist/compiler/check-unit.d.ts +26 -2
- package/dist/compiler/compile.d.ts +4 -0
- package/dist/compiler/compile.js +314 -30
- package/dist/compiler/diagnostic-docs.js +20 -5
- package/dist/compiler/diagnostics.d.ts +3 -0
- package/dist/compiler/diagnostics.js +3 -0
- package/dist/compiler/expr.d.ts +24 -0
- package/dist/compiler/expr.js +53 -8
- package/dist/compiler/index.d.ts +3 -2
- package/dist/compiler/index.js +70 -16
- package/dist/compiler/vocabulary.d.ts +1 -0
- package/dist/compiler/vocabulary.js +9 -3
- package/dist/each.d.ts +3 -0
- package/dist/each.js +121 -0
- package/dist/index.d.ts +1 -1
- package/dist/ir.d.ts +30 -1
- package/dist/listbox.d.ts +3 -0
- package/dist/listbox.js +66 -0
- package/dist/lsp/typescript.js +1 -1
- package/dist/lsp/workspace.js +3 -2
- package/dist/paginate.js +18 -4
- package/dist/project.d.ts +6 -0
- package/dist/project.js +22 -19
- package/dist/runtime/app.d.ts +35 -1
- package/dist/runtime/app.js +178 -40
- package/dist/runtime/config.d.ts +10 -3
- package/dist/runtime/config.js +10 -3
- package/dist/runtime/request.d.ts +1 -1
- package/dist/runtime/request.js +6 -1
- package/dist/runtime/store.d.ts +7 -4
- package/dist/runtime/store.js +154 -41
- package/dist/sse.js +8 -5
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +17 -2
- package/dist/vite.js +4 -2
- package/docs/diagnostics.md +35 -5
- package/html-custom-data.json +46 -2
- package/llms.txt +112 -6
- package/package.json +6 -1
package/dist/compiler/compile.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// The compiler: one pure, deterministic function from annotated HTML to a
|
|
2
2
|
// rewritten page plus its IR and function table.
|
|
3
3
|
import * as parse5 from 'parse5';
|
|
4
|
+
import { parseExpressionAt } from 'acorn';
|
|
4
5
|
import { didYouMean, severityOf } from './diagnostics.js';
|
|
5
6
|
import { analyze } from './expr.js';
|
|
6
7
|
import { ATTRIBUTES, ELEMENTS } from './vocabulary.js';
|
|
7
8
|
import { matchOperation, matchResource, queryNames, walk } from './openapi.js';
|
|
8
9
|
const ATTRS = ATTRIBUTES.map((a) => a.name);
|
|
9
10
|
const METHODS = ['get', 'post', 'put', 'patch', 'delete'];
|
|
10
|
-
const ACTION_ONLY = ['body', 'trigger', 'debounce', 'concurrency', 'optimistic', 'then', 'catch'];
|
|
11
|
+
const ACTION_ONLY = ['body', 'trigger', 'debounce', 'concurrency', 'optimistic', 'then', 'catch', 'confirm', 'encoding'];
|
|
11
12
|
const UNSAFE_ELEMENTS = new Set(['script', 'style', 'iframe', 'object', 'embed', 'base', 'meta', 'link', 'animate', 'set', 'animatemotion', 'animatetransform']);
|
|
12
13
|
const FORM_CONTROLS = new Set(['input', 'select', 'textarea']);
|
|
13
14
|
const RESOURCE_ATTRS = ELEMENTS.find((e) => e.name === 'sn-resource').attrs;
|
|
@@ -23,6 +24,7 @@ class Compiler {
|
|
|
23
24
|
diagnostics = [];
|
|
24
25
|
files = new Set();
|
|
25
26
|
fns = [];
|
|
27
|
+
fnIndex = new Map();
|
|
26
28
|
origins = new WeakMap();
|
|
27
29
|
lineStarts = new Map();
|
|
28
30
|
page;
|
|
@@ -35,6 +37,8 @@ class Compiler {
|
|
|
35
37
|
remove = [];
|
|
36
38
|
embedded = new Map();
|
|
37
39
|
features = new Set();
|
|
40
|
+
/** action functions called by the page (`archive`, `pgp.decrypt`) */
|
|
41
|
+
calls = new Set();
|
|
38
42
|
nextId = 0;
|
|
39
43
|
nextSid = 0;
|
|
40
44
|
check;
|
|
@@ -50,7 +54,8 @@ class Compiler {
|
|
|
50
54
|
ns.set(k, new Set(v.members));
|
|
51
55
|
this.page = {
|
|
52
56
|
resources: new Map(), helpers: new Set(opts.helpers.names), namespaces: ns,
|
|
53
|
-
|
|
57
|
+
actions: new Set([...(opts.actions?.names ?? []), ...Object.keys(opts.actions?.namespaces ?? {})]),
|
|
58
|
+
usedHelpers: new Set(), usedNs: new Map(), usesNow: false, pathParams: new Set(), dialogs: new Set(),
|
|
54
59
|
};
|
|
55
60
|
}
|
|
56
61
|
run(html) {
|
|
@@ -95,8 +100,8 @@ class Compiler {
|
|
|
95
100
|
return {
|
|
96
101
|
html: parse5.serialize(doc),
|
|
97
102
|
ir, fns: this.fns,
|
|
98
|
-
features: [...(this.opts.dev ? ['dev'] : []), ...['sse', 'optimistic', 'paginate', 'persist', 'cache'].filter((f) => this.features.has(f))],
|
|
99
|
-
helpers: { user, builtin }, namespaces,
|
|
103
|
+
features: [...(this.opts.dev ? ['dev'] : []), ...['sse', 'optimistic', 'paginate', 'persist', 'cache', 'each', 'listbox'].filter((f) => this.features.has(f))],
|
|
104
|
+
helpers: { user, builtin }, namespaces, calls: [...this.calls].sort(),
|
|
100
105
|
diagnostics: this.diagnostics, files: [...this.files].sort(),
|
|
101
106
|
...(this.check ? { check: this.check } : {}),
|
|
102
107
|
};
|
|
@@ -153,6 +158,93 @@ class Compiler {
|
|
|
153
158
|
return i;
|
|
154
159
|
}
|
|
155
160
|
/** `inExpr`: the offset points into an expression; otherwise offset 0 is about the whole attribute (or element) */
|
|
161
|
+
/**
|
|
162
|
+
* `data-sn-call` / `call`: exactly one call of an actions module function, `name(args)` or
|
|
163
|
+
* `ns.name(args)`. Compiles to two table entries: the arguments as one array expression, and a
|
|
164
|
+
* runner that calls the imported function with them and the context.
|
|
165
|
+
*/
|
|
166
|
+
callExpr(el, attrName, scope, deps, magic) {
|
|
167
|
+
const text = attr(el, attrName) ?? '';
|
|
168
|
+
const shape = `\`${attrName}\` is one call of an action, e.g. \`pgp.decrypt(msg)\``;
|
|
169
|
+
let ast;
|
|
170
|
+
try {
|
|
171
|
+
ast = parseExpressionAt(text, 0, { ecmaVersion: 'latest' });
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
this.report(el, attrName, 'SN401', `\`${attrName}\`: ${e.message.replace(/ \(\d+:\d+\)$/, '')}`, e.pos ?? 0);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (ast.type !== 'CallExpression' || ast.optional || ast.end !== text.trimEnd().length) {
|
|
178
|
+
this.report(el, attrName, 'SN401', shape, 0);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const acts = this.opts.actions ?? { module: null, names: [], namespaces: {} };
|
|
182
|
+
const callee = ast.callee;
|
|
183
|
+
const all = [...acts.names, ...Object.entries(acts.namespaces).flatMap(([ns, m]) => m.members.map((x) => `${ns}.${x}`))];
|
|
184
|
+
let name, local;
|
|
185
|
+
if (callee.type === 'Identifier') {
|
|
186
|
+
name = callee.name;
|
|
187
|
+
local = `$a$${name}`;
|
|
188
|
+
if (!acts.names.includes(name)) {
|
|
189
|
+
this.report(el, attrName, 'SN310', `\`${name}\` is not exported by the actions module.${didYouMean(name, all)}`, callee.start);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (callee.type === 'MemberExpression' && !callee.computed && callee.object.type === 'Identifier' && callee.property.type === 'Identifier') {
|
|
194
|
+
name = `${callee.object.name}.${callee.property.name}`;
|
|
195
|
+
local = `$a$${callee.object.name}$${callee.property.name}`;
|
|
196
|
+
if (!acts.namespaces[callee.object.name]?.members.includes(callee.property.name)) {
|
|
197
|
+
this.report(el, attrName, 'SN310', `\`${name}\` is not exported by the actions module.${didYouMean(name, all)}`, callee.start);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
this.report(el, attrName, 'SN401', shape, callee.start);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const open = text.indexOf('(', callee.end);
|
|
206
|
+
const r = this.expr(el, attrName, scope, { mode: 'value', ...(magic ? { magic } : {}) }, `[${text.slice(open + 1, ast.end - 1)}]`, open);
|
|
207
|
+
if (this.check)
|
|
208
|
+
this.typeLast({ returns: { kind: 'call', name, args: true } });
|
|
209
|
+
if (deps)
|
|
210
|
+
for (const d of r.deps)
|
|
211
|
+
deps.add(d);
|
|
212
|
+
if (!r.ok)
|
|
213
|
+
return null;
|
|
214
|
+
this.calls.add(name);
|
|
215
|
+
return { args: this.fn(r.code), run: this.fn(`($a, $c) => ${local}(...$a, $c)`), name };
|
|
216
|
+
}
|
|
217
|
+
/** `direction-values="{prev: 'backward', next: 'forward'}"`: static data, string values for known directions only */
|
|
218
|
+
directionValues(el) {
|
|
219
|
+
const text = attr(el, 'direction-values');
|
|
220
|
+
const DIRS = ['first', 'prev', 'next', 'last'];
|
|
221
|
+
const fail = (message, offset = 0) => { this.report(el, 'direction-values', 'SN401', message, offset); return null; };
|
|
222
|
+
let ast;
|
|
223
|
+
try {
|
|
224
|
+
ast = parseExpressionAt(text, 0, { ecmaVersion: 'latest' });
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
return fail(`\`direction-values\`: ${e.message.replace(/ \(\d+:\d+\)$/, '')}`, e.pos ?? 0);
|
|
228
|
+
}
|
|
229
|
+
if (ast.type !== 'ObjectExpression' || ast.end !== text.trimEnd().length) {
|
|
230
|
+
return fail("`direction-values` is an object literal of strings, e.g. `{prev: 'backward', next: 'forward'}`");
|
|
231
|
+
}
|
|
232
|
+
const out = {};
|
|
233
|
+
for (const p of ast.properties) {
|
|
234
|
+
if (p.type !== 'Property' || p.computed || p.kind !== 'init' || p.method || p.shorthand) {
|
|
235
|
+
return fail("`direction-values` is an object literal of strings, e.g. `{prev: 'backward', next: 'forward'}`", p.start);
|
|
236
|
+
}
|
|
237
|
+
const key = p.key.type === 'Identifier' ? p.key.name : p.key.type === 'Literal' ? String(p.key.value) : '';
|
|
238
|
+
if (!DIRS.includes(key)) {
|
|
239
|
+
return fail(`\`${key}\` is not a direction; directions are \`first\`, \`prev\`, \`next\` and \`last\`.${didYouMean(key, DIRS)}`, p.key.start);
|
|
240
|
+
}
|
|
241
|
+
if (p.value.type !== 'Literal' || typeof p.value.value !== 'string') {
|
|
242
|
+
return fail(`the value for \`${key}\` must be a string literal`, p.value.start);
|
|
243
|
+
}
|
|
244
|
+
out[key] = p.value.value;
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
156
248
|
report(el, attr, code, message, offset, hint, inExpr = false) {
|
|
157
249
|
const origin = (el && this.origins.get(el)) || { file: this.opts.file, src: '', chain: [] };
|
|
158
250
|
const base = el ? this.valueOffset(el, attr) : 0;
|
|
@@ -229,7 +321,7 @@ class Compiler {
|
|
|
229
321
|
// the browser merges adjacent text when it parses the output; merge now so node paths agree
|
|
230
322
|
mergeText(parent);
|
|
231
323
|
}
|
|
232
|
-
// ---- declarations (first pass: names only, so expressions can see every resource)
|
|
324
|
+
// ---- declarations (first pass: names only, so expressions can see every resource and dialog)
|
|
233
325
|
declare(parent, inTemplate = false) {
|
|
234
326
|
for (const child of parent.childNodes) {
|
|
235
327
|
if (!isElement(child))
|
|
@@ -251,10 +343,17 @@ class Compiler {
|
|
|
251
343
|
const key = attr(child, 'key');
|
|
252
344
|
const paginate = attr(child, 'paginate');
|
|
253
345
|
const info = { name, key: key ? key.split(',').map((s) => s.trim()).filter(Boolean) : null, paginate: paginate ?? null, used: false };
|
|
346
|
+
if (paginate === 'keyset' && attr(child, 'offset-param') != null)
|
|
347
|
+
info.jumps = true;
|
|
254
348
|
this.page.resources.set(name, info);
|
|
255
349
|
this.resourceEls.set(name, child);
|
|
256
350
|
continue;
|
|
257
351
|
}
|
|
352
|
+
if (child.tagName === 'dialog') {
|
|
353
|
+
const id = attr(child, 'id');
|
|
354
|
+
if (id)
|
|
355
|
+
this.page.dialogs.add(id);
|
|
356
|
+
}
|
|
258
357
|
this.declare(child, inTemplate);
|
|
259
358
|
if (child.tagName === 'template')
|
|
260
359
|
this.declare(child.content, true);
|
|
@@ -318,7 +417,7 @@ class Compiler {
|
|
|
318
417
|
// unknown attributes
|
|
319
418
|
const known = new Set(ATTRS.map((a) => `data-sn-${a}`));
|
|
320
419
|
for (const a of sn) {
|
|
321
|
-
if (known.has(a.name) || a.name.startsWith('data-sn-on-'))
|
|
420
|
+
if (known.has(a.name) || a.name.startsWith('data-sn-on-') || a.name.startsWith('data-sn-debounce-'))
|
|
322
421
|
continue;
|
|
323
422
|
this.report(el, a.name, 'SN101', `\`${a.name}\` is not a sinter attribute.${didYouMean(a.name, [...known, 'data-sn-on-click'])}`, 0);
|
|
324
423
|
}
|
|
@@ -352,6 +451,9 @@ class Compiler {
|
|
|
352
451
|
return null;
|
|
353
452
|
}
|
|
354
453
|
const node = { tag: el.tagName, p: ctx.stateIdx, b: [] };
|
|
454
|
+
const inDialog = ctx.dialog || el.tagName === 'dialog';
|
|
455
|
+
if (inDialog)
|
|
456
|
+
node.dl = 1;
|
|
355
457
|
const nodeIdx = ctx.list.length;
|
|
356
458
|
let scope = ctx.scope;
|
|
357
459
|
let result = null;
|
|
@@ -369,13 +471,32 @@ class Compiler {
|
|
|
369
471
|
const locs = first.map((e) => this.locOf(el, 'data-sn-state', e.offset));
|
|
370
472
|
this.check.frames.push({ kind: 'state', names: [...entry.names], init: this.lastExpr, locs });
|
|
371
473
|
}
|
|
372
|
-
|
|
474
|
+
this.checkNames(el, 'data-sn-state', scope, [...entry.names]);
|
|
475
|
+
scope = { ...scope, states: [...scope.states, entry] };
|
|
373
476
|
if (has('persist') && !has('model')) {
|
|
374
477
|
node.persist = get('persist');
|
|
375
478
|
this.features.add('persist');
|
|
376
479
|
}
|
|
377
480
|
}
|
|
378
481
|
}
|
|
482
|
+
// derived names: the element and its subtree read them like state
|
|
483
|
+
if (has('let'))
|
|
484
|
+
scope = this.lets(el, scope);
|
|
485
|
+
// a text field with suggestions: the listbox feature handles the keys (see src/listbox.ts)
|
|
486
|
+
if (has('listbox')) {
|
|
487
|
+
const list = get('listbox');
|
|
488
|
+
if (!['input', 'textarea'].includes(el.tagName))
|
|
489
|
+
this.report(el, 'data-sn-listbox', 'SN102', '`data-sn-listbox` belongs on a text `<input>` or `<textarea>`', 0);
|
|
490
|
+
else if (list && !/^[A-Za-z][\w-]*$/.test(list))
|
|
491
|
+
this.report(el, 'data-sn-listbox', 'SN401', '`data-sn-listbox` takes the literal `id` of the suggestion list, or nothing for the nearest `[role="listbox"]` around the input', 0);
|
|
492
|
+
else {
|
|
493
|
+
setAttr(el, 'role', 'combobox');
|
|
494
|
+
if (list)
|
|
495
|
+
setAttr(el, 'aria-controls', list);
|
|
496
|
+
setAttr(el, 'aria-autocomplete', 'list');
|
|
497
|
+
this.features.add('listbox');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
379
500
|
const isTemplate = el.tagName === 'template';
|
|
380
501
|
const valueB = (k, name) => {
|
|
381
502
|
const r = this.expr(el, name, scope, { mode: 'value' });
|
|
@@ -408,6 +529,27 @@ class Compiler {
|
|
|
408
529
|
this.model(el, node, scope, get('persist'));
|
|
409
530
|
else if (has('persist') && !has('state'))
|
|
410
531
|
this.report(el, 'data-sn-persist', 'SN102', '`data-sn-persist` needs `data-sn-model` or `data-sn-state` on the same element', 0);
|
|
532
|
+
const methods = METHODS.filter((m) => has(m));
|
|
533
|
+
const isCall = has('call');
|
|
534
|
+
// without a method, `data-sn-debounce` delays the element's own handlers
|
|
535
|
+
let handlerDebounce = 0;
|
|
536
|
+
if (!methods.length && !isCall && has('debounce')) {
|
|
537
|
+
handlerDebounce = Number(get('debounce'));
|
|
538
|
+
if (!Number.isFinite(handlerDebounce) || handlerDebounce < 0)
|
|
539
|
+
this.report(el, 'data-sn-debounce', 'SN401', '`data-sn-debounce` takes milliseconds', 0);
|
|
540
|
+
if (!sn.some((a) => a.name.startsWith('data-sn-on-')))
|
|
541
|
+
this.report(el, 'data-sn-debounce', 'SN102', '`data-sn-debounce` needs a method attribute (`data-sn-get`, `-post`, …) or a `data-sn-on-*` handler on the same element', 0);
|
|
542
|
+
}
|
|
543
|
+
// `data-sn-debounce-<event>`: only that event's handler waits
|
|
544
|
+
for (const a of sn) {
|
|
545
|
+
if (!a.name.startsWith('data-sn-debounce-'))
|
|
546
|
+
continue;
|
|
547
|
+
const event = a.name.slice('data-sn-debounce-'.length), ms = Number(a.value);
|
|
548
|
+
if (!Number.isFinite(ms) || ms < 0 || a.value.trim() === '')
|
|
549
|
+
this.report(el, a.name, 'SN401', `\`${a.name}\` takes milliseconds`, 0);
|
|
550
|
+
if (!has(`on-${event}`))
|
|
551
|
+
this.report(el, a.name, 'SN102', `\`${a.name}\` needs a \`data-sn-on-${event}\` handler on the same element`, 0);
|
|
552
|
+
}
|
|
411
553
|
for (const a of sn) {
|
|
412
554
|
if (!a.name.startsWith('data-sn-on-'))
|
|
413
555
|
continue;
|
|
@@ -417,17 +559,21 @@ class Compiler {
|
|
|
417
559
|
continue;
|
|
418
560
|
}
|
|
419
561
|
const r = this.expr(el, a.name, scope, { mode: 'handler', magic: ['$event'] });
|
|
562
|
+
const own = get(`debounce-${event}`), ms = own !== undefined ? Number(own) || 0 : handlerDebounce;
|
|
420
563
|
if (r.ok)
|
|
421
|
-
node.b.push(binding({ k: 'on', n: event, f: this.fn(r.code), w: r.writes }));
|
|
564
|
+
node.b.push(binding({ k: 'on', n: event, f: this.fn(r.code), w: r.writes, ...(ms > 0 ? { ms } : {}) }));
|
|
422
565
|
}
|
|
423
|
-
const methods = METHODS.filter((m) => has(m));
|
|
424
566
|
if (methods.length > 1)
|
|
425
567
|
this.report(el, `data-sn-${methods[1]}`, 'SN102', 'an element can have only one method attribute', 0);
|
|
568
|
+
if (methods.length && isCall)
|
|
569
|
+
this.report(el, 'data-sn-call', 'SN102', 'an element has one action: a method attribute or `data-sn-call`, not both', 0);
|
|
426
570
|
if (methods.length)
|
|
427
571
|
this.action(el, node, scope, methods[0], get);
|
|
572
|
+
else if (isCall)
|
|
573
|
+
this.action(el, node, scope, 'call', get);
|
|
428
574
|
else
|
|
429
575
|
for (const n of ACTION_ONLY)
|
|
430
|
-
if (has(n))
|
|
576
|
+
if (has(n) && n !== 'debounce')
|
|
431
577
|
this.report(el, `data-sn-${n}`, 'SN102', `\`data-sn-${n}\` needs a method attribute (\`data-sn-get\`, \`-post\`, …) on the same element`, 0);
|
|
432
578
|
// templates
|
|
433
579
|
let content = null;
|
|
@@ -476,7 +622,7 @@ class Compiler {
|
|
|
476
622
|
}
|
|
477
623
|
else {
|
|
478
624
|
this.walkChildren(el, {
|
|
479
|
-
...ctx, scope,
|
|
625
|
+
...ctx, scope, dialog: inDialog,
|
|
480
626
|
stateIdx: node.state !== undefined && hasDirectives ? nodeIdx : ctx.stateIdx,
|
|
481
627
|
path: ctx.tid >= 0 ? [...ctx.path, index] : ctx.path,
|
|
482
628
|
});
|
|
@@ -495,9 +641,12 @@ class Compiler {
|
|
|
495
641
|
this.templates.push({ id: tid, nodes: [] });
|
|
496
642
|
return tid;
|
|
497
643
|
}
|
|
644
|
+
/** Index of a function in the page's table; identical code shares one entry (one compile in the engine). */
|
|
498
645
|
fn(code) {
|
|
499
|
-
this.
|
|
500
|
-
|
|
646
|
+
let i = this.fnIndex.get(code);
|
|
647
|
+
if (i === undefined)
|
|
648
|
+
this.fnIndex.set(code, (i = this.fns.push(code) - 1));
|
|
649
|
+
return i;
|
|
501
650
|
}
|
|
502
651
|
expr(el, attrName, scope, o, value, shift = 0) {
|
|
503
652
|
const src = value ?? attr(el, attrName) ?? '';
|
|
@@ -513,6 +662,61 @@ class Compiler {
|
|
|
513
662
|
}
|
|
514
663
|
return r;
|
|
515
664
|
}
|
|
665
|
+
// ---- derived names
|
|
666
|
+
/**
|
|
667
|
+
* `data-sn-let="{ s: suggestions(q), n: s.length }"`: each name is a value expression, inlined
|
|
668
|
+
* where it is read; later names may use earlier ones.
|
|
669
|
+
*/
|
|
670
|
+
lets(el, scope) {
|
|
671
|
+
const src = attr(el, 'data-sn-let') ?? '';
|
|
672
|
+
let obj;
|
|
673
|
+
try {
|
|
674
|
+
obj = parseExpressionAt(src, 0, { ecmaVersion: 'latest' });
|
|
675
|
+
}
|
|
676
|
+
catch (e) {
|
|
677
|
+
this.report(el, 'data-sn-let', 'SN401', e.message.replace(/ \(\d+:\d+\)$/, ''), e.pos ?? 0);
|
|
678
|
+
return scope;
|
|
679
|
+
}
|
|
680
|
+
if (obj.type !== 'ObjectExpression') {
|
|
681
|
+
this.report(el, 'data-sn-let', 'SN401', '`data-sn-let` takes an object literal: `{ name: expression, … }`', 0);
|
|
682
|
+
return scope;
|
|
683
|
+
}
|
|
684
|
+
for (const p of obj.properties) {
|
|
685
|
+
if (p.type !== 'Property' || p.computed || p.shorthand || p.key.type !== 'Identifier' || p.kind !== 'init' || p.method) {
|
|
686
|
+
this.report(el, 'data-sn-let', 'SN401', '`data-sn-let` entries are `name: expression`', p.start);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const name = p.key.name;
|
|
690
|
+
if (name.startsWith('$')) {
|
|
691
|
+
this.report(el, 'data-sn-let', 'SN401', 'names starting with `$` are reserved', p.key.start);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (!this.checkNames(el, 'data-sn-let', scope, [name], p.key.start))
|
|
695
|
+
continue;
|
|
696
|
+
const v = p.value;
|
|
697
|
+
const r = this.expr(el, 'data-sn-let', scope, { mode: 'value' }, src.slice(v.start, v.end), v.start);
|
|
698
|
+
const entry = { name, code: r.inner, deps: r.deps, usesEl: r.usesEl, usesIndex: r.usesIndex, resource: r.resource };
|
|
699
|
+
if (this.check) {
|
|
700
|
+
this.frameOf.set(entry, this.check.frames.length);
|
|
701
|
+
this.check.frames.push({ kind: 'let', name, init: r.ok ? this.lastExpr : null, loc: this.locOf(el, 'data-sn-let', p.key.start) });
|
|
702
|
+
}
|
|
703
|
+
scope = { ...scope, lets: [...(scope.lets ?? []), entry] };
|
|
704
|
+
}
|
|
705
|
+
return scope;
|
|
706
|
+
}
|
|
707
|
+
/** A name declared here must not hide a let, or be hidden by one (SN209); false on a conflict. */
|
|
708
|
+
checkNames(el, attrName, scope, names, offset = 0) {
|
|
709
|
+
let ok = true;
|
|
710
|
+
for (const n of names) {
|
|
711
|
+
const isLet = (scope.lets ?? []).some((l) => l.name === n);
|
|
712
|
+
const other = attrName === 'data-sn-let' && (scope.loops.some((l) => l.name === n) || scope.states.some((s) => s.names.has(n)));
|
|
713
|
+
if (isLet || other) {
|
|
714
|
+
this.report(el, attrName, 'SN209', `\`${n}\` is already defined here${isLet ? ' by `data-sn-let`' : ''}; use another name`, offset);
|
|
715
|
+
ok = false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return ok;
|
|
719
|
+
}
|
|
516
720
|
// ---- check unit
|
|
517
721
|
/** Adds type information to the expression `expr()` recorded last. */
|
|
518
722
|
typeLast(info) {
|
|
@@ -533,7 +737,7 @@ class Compiler {
|
|
|
533
737
|
}
|
|
534
738
|
recordExpr(el, attrName, scope, o, src, shift, r) {
|
|
535
739
|
const frames = [];
|
|
536
|
-
for (const f of [...scope.states, ...scope.loops]) {
|
|
740
|
+
for (const f of [...scope.states, ...scope.loops, ...(scope.lets ?? [])]) {
|
|
537
741
|
const i = this.frameOf.get(f);
|
|
538
742
|
if (i !== undefined && !frames.includes(i))
|
|
539
743
|
frames.push(i);
|
|
@@ -541,7 +745,7 @@ class Compiler {
|
|
|
541
745
|
let event = null;
|
|
542
746
|
if (attrName.startsWith('data-sn-on-'))
|
|
543
747
|
event = attrName.slice('data-sn-on-'.length);
|
|
544
|
-
else if (o.mode !== 'value') {
|
|
748
|
+
else if (o.mode !== 'value' || o.magic?.includes('$event')) {
|
|
545
749
|
event = attr(el, 'data-sn-trigger') ?? (el.tagName === 'form' ? 'submit' : FORM_CONTROLS.has(el.tagName) ? 'change' : 'click');
|
|
546
750
|
}
|
|
547
751
|
const e = {
|
|
@@ -549,13 +753,15 @@ class Compiler {
|
|
|
549
753
|
result: attrName === 'data-sn-then', error: attrName === 'data-sn-catch',
|
|
550
754
|
};
|
|
551
755
|
if (o.payload)
|
|
552
|
-
e.payload = { names: r.payloadNames ?? [], type: null };
|
|
756
|
+
e.payload = { names: r.payloadNames ?? [], type: null, ...(r.payloadStates?.length ? { states: r.payloadStates } : {}) };
|
|
553
757
|
this.check.exprs.push(e);
|
|
554
758
|
return this.check.exprs.length - 1;
|
|
555
759
|
}
|
|
556
760
|
// ---- bindings
|
|
557
761
|
attrBinding(el, node, scope) {
|
|
558
762
|
const r = this.expr(el, 'data-sn-attr', scope, { mode: 'value', object: 'literal-keys' });
|
|
763
|
+
if (this.check)
|
|
764
|
+
this.typeLast({ returns: { kind: 'decl', name: 'Attrs' } });
|
|
559
765
|
if (!r.entries)
|
|
560
766
|
return;
|
|
561
767
|
for (const e of r.entries) {
|
|
@@ -649,7 +855,11 @@ class Compiler {
|
|
|
649
855
|
node.b.push(b);
|
|
650
856
|
}
|
|
651
857
|
action(el, node, scope, method, get) {
|
|
652
|
-
|
|
858
|
+
// arguments and body are taken when the trigger fires, so they see its event
|
|
859
|
+
const call = method === 'call' ? this.callExpr(el, 'data-sn-call', scope, undefined, ['$event']) : null;
|
|
860
|
+
if (method === 'call' && get('body') !== undefined)
|
|
861
|
+
this.report(el, 'data-sn-body', 'SN102', '`data-sn-body` belongs to a method attribute; a call takes its arguments in `data-sn-call`', 0);
|
|
862
|
+
const url = method === 'call' ? [] : this.urlTemplate(el, `data-sn-${method}`, scope);
|
|
653
863
|
const tag = el.tagName;
|
|
654
864
|
const isForm = tag === 'form';
|
|
655
865
|
const trigger = get('trigger') ?? (isForm ? 'submit' : FORM_CONTROLS.has(tag) ? 'change' : 'click');
|
|
@@ -674,17 +884,27 @@ class Compiler {
|
|
|
674
884
|
return { f: r.ok ? this.fn(r.code) : null, w: r.writes };
|
|
675
885
|
};
|
|
676
886
|
// with OpenAPI specs: the body must fit the request schema, `$result` gets the response's type
|
|
677
|
-
const op = this.check && this.opts.openapi?.length ? matchOperation(this.opts.openapi, url, method) : null;
|
|
887
|
+
const op = method !== 'call' && this.check && this.opts.openapi?.length ? matchOperation(this.opts.openapi, url, method) : null;
|
|
678
888
|
const typed = op && 'spec' in op ? op : null;
|
|
679
|
-
const body = fnOf('body', { mode: 'value' }, typed?.request != null ? { returns: { kind: 'openapi', spec: typed.spec, schema: typed.request } } : undefined);
|
|
889
|
+
const body = fnOf('body', { mode: 'value', magic: ['$event'] }, typed?.request != null ? { returns: { kind: 'openapi', spec: typed.spec, schema: typed.request } } : undefined);
|
|
680
890
|
const optimistic = fnOf('optimistic', { mode: 'optimistic' });
|
|
681
891
|
if (optimistic.f !== null)
|
|
682
892
|
this.features.add('optimistic');
|
|
683
|
-
const then = fnOf('then', { mode: 'handler', magic: ['$event', '$result'] }, typed?.body != null ? { resultType: { kind: 'openapi', spec: typed.spec, schema: typed.body } }
|
|
893
|
+
const then = fnOf('then', { mode: 'handler', magic: ['$event', '$result'] }, typed?.body != null ? { resultType: { kind: 'openapi', spec: typed.spec, schema: typed.body } }
|
|
894
|
+
: call ? { resultType: { kind: 'call', name: call.name } } : undefined);
|
|
684
895
|
const catch_ = fnOf('catch', { mode: 'handler', magic: ['$event', '$error'] });
|
|
896
|
+
const confirm = fnOf('confirm', { mode: 'value', magic: ['$event'] });
|
|
897
|
+
const encoding = get('encoding');
|
|
898
|
+
if (encoding !== undefined && encoding !== 'form' && encoding !== 'json')
|
|
899
|
+
this.report(el, 'data-sn-encoding', 'SN401', '`data-sn-encoding` is `json` (the default) or `form`', 0);
|
|
900
|
+
if (encoding !== undefined && method === 'call')
|
|
901
|
+
this.report(el, 'data-sn-encoding', 'SN102', '`data-sn-encoding` belongs to a method attribute; a call sends its own requests', 0);
|
|
685
902
|
const def = {
|
|
686
903
|
method: method.toUpperCase(), url, body: body.f, trigger, debounce, concurrency,
|
|
687
904
|
optimistic: optimistic.f, then: then.f, catch: catch_.f, w: [...new Set([...then.w, ...catch_.w])].sort(), form: isForm,
|
|
905
|
+
...(call ? { call: { args: call.args, run: call.run } } : {}),
|
|
906
|
+
...(confirm.f !== null ? { confirm: confirm.f } : {}),
|
|
907
|
+
...(encoding === 'form' ? { enc: 'form' } : {}),
|
|
688
908
|
};
|
|
689
909
|
this.actions.push(def);
|
|
690
910
|
node.b.push(binding({ k: 'on', n: trigger, x: this.actions.length - 1 }));
|
|
@@ -719,7 +939,8 @@ class Compiler {
|
|
|
719
939
|
frame.indexLoc = this.locOf(el, 'data-sn-for', value.indexOf(index, itemAt + item.length));
|
|
720
940
|
this.check.frames.push(frame);
|
|
721
941
|
}
|
|
722
|
-
|
|
942
|
+
this.checkNames(el, 'data-sn-for', scope, index ? [item, index] : [item]);
|
|
943
|
+
const inner = { ...scope, loops };
|
|
723
944
|
t.key = null;
|
|
724
945
|
t.keyFields = null;
|
|
725
946
|
if (keySrc !== undefined) {
|
|
@@ -803,10 +1024,50 @@ class Compiler {
|
|
|
803
1024
|
this.report(el, a.name, 'SN101', `\`${a.name}\` is not an attribute of \`<sn-resource>\`.${didYouMean(a.name, RESOURCE_ATTRS)}`, 0);
|
|
804
1025
|
}
|
|
805
1026
|
const info = this.page.resources.get(name);
|
|
806
|
-
|
|
807
|
-
|
|
1027
|
+
const hasCall = attr(el, 'call') != null;
|
|
1028
|
+
if (hasCall && attr(el, 'src') != null)
|
|
1029
|
+
this.report(el, 'call', 'SN102', 'a resource loads from `src` or from `call`, not both', 0);
|
|
1030
|
+
else if (!hasCall && attr(el, 'src') == null)
|
|
1031
|
+
this.report(el, null, 'SN401', `resource \`${name}\` needs a \`src\` (or a \`call\`)`, 0);
|
|
1032
|
+
if (hasCall) {
|
|
1033
|
+
for (const a of ['paginate', 'each', 'cache']) {
|
|
1034
|
+
if (attr(el, a) != null)
|
|
1035
|
+
this.report(el, a, 'SN401', `\`${a}\` does not combine with \`call\` in v0.2`, 0);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
808
1038
|
const deps = new Set();
|
|
809
|
-
|
|
1039
|
+
// `each="a in accounts"`: src is evaluated once per list item, with the item in scope
|
|
1040
|
+
let each;
|
|
1041
|
+
let srcScope = ctx.scope;
|
|
1042
|
+
const eachAttr = attr(el, 'each');
|
|
1043
|
+
if (eachAttr != null) {
|
|
1044
|
+
const m = /^\s*([A-Za-z_$][\w$]*)\s+in\s+/.exec(eachAttr);
|
|
1045
|
+
if (!m)
|
|
1046
|
+
this.report(el, 'each', 'SN401', '`each` takes `item in list`, e.g. `a in accounts`', 0);
|
|
1047
|
+
else {
|
|
1048
|
+
const list = this.expr(el, 'each', ctx.scope, { mode: 'value' }, eachAttr.slice(m[0].length), m[0].length);
|
|
1049
|
+
const listExpr = this.lastExpr;
|
|
1050
|
+
for (const d of list.deps)
|
|
1051
|
+
deps.add(d);
|
|
1052
|
+
const loop = { name: m[1], kind: 'item', alias: null, resource: list.resource };
|
|
1053
|
+
if (this.check) {
|
|
1054
|
+
this.frameOf.set(loop, this.check.frames.length);
|
|
1055
|
+
this.check.frames.push({ kind: 'loop', item: m[1], index: null, init: listExpr, loc: this.locOf(el, 'each', eachAttr.indexOf(m[1])) });
|
|
1056
|
+
}
|
|
1057
|
+
srcScope = { ...ctx.scope, loops: [...ctx.scope.loops, loop] };
|
|
1058
|
+
if (list.ok)
|
|
1059
|
+
each = { item: m[1], list: this.fn(list.code), ...(list.resource ? { of: list.resource } : {}) };
|
|
1060
|
+
this.features.add('each');
|
|
1061
|
+
if (!info.key)
|
|
1062
|
+
this.report(el, 'each', 'SN401', 'an `each` resource is a collection: it needs `key`, with a field that tells the sources apart', 0);
|
|
1063
|
+
for (const a of ['paginate', 'total', 'cache']) {
|
|
1064
|
+
if (attr(el, a) != null)
|
|
1065
|
+
this.report(el, a, 'SN401', `\`${a}\` does not combine with \`each\`: the items come from several requests`, 0);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const src = hasCall ? [] : this.urlTemplate(el, 'src', srcScope, {}, deps);
|
|
1070
|
+
const call = hasCall ? this.callExpr(el, 'call', ctx.scope, deps) : null;
|
|
810
1071
|
let when = null;
|
|
811
1072
|
if (attr(el, 'when') != null) {
|
|
812
1073
|
const r = this.expr(el, 'when', ctx.scope, { mode: 'value' });
|
|
@@ -842,10 +1103,28 @@ class Compiler {
|
|
|
842
1103
|
mode: mode, size, sizeParam: attr(el, 'size-param') ?? 'limit', cursor,
|
|
843
1104
|
directionParam: attr(el, 'direction-param') ?? 'direction', pageParam: attr(el, 'page-param') ?? 'page',
|
|
844
1105
|
};
|
|
1106
|
+
const offsetParam = attr(el, 'offset-param');
|
|
1107
|
+
if (offsetParam != null) {
|
|
1108
|
+
if (mode !== 'keyset')
|
|
1109
|
+
this.report(el, 'offset-param', 'SN401', `\`offset-param\` applies to \`paginate="keyset"\`; ${mode === 'offset' ? 'offset pages jump with `page-param`' : 'append mode only loads more'}`, 0);
|
|
1110
|
+
else if (!/^[\w.\-\[\]]+$/.test(offsetParam))
|
|
1111
|
+
this.report(el, 'offset-param', 'SN401', '`offset-param` is a query parameter name', 0);
|
|
1112
|
+
else
|
|
1113
|
+
paginate.offsetParam = offsetParam;
|
|
1114
|
+
}
|
|
1115
|
+
if (attr(el, 'direction-values') != null) {
|
|
1116
|
+
if (mode === 'offset')
|
|
1117
|
+
this.report(el, 'direction-values', 'SN401', '`direction-values` applies to `paginate="keyset"` and `paginate="append"`; offset pages send `page-param`', 0);
|
|
1118
|
+
else {
|
|
1119
|
+
const values = this.directionValues(el);
|
|
1120
|
+
if (values)
|
|
1121
|
+
paginate.directionValues = values;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
845
1124
|
this.features.add('paginate');
|
|
846
1125
|
}
|
|
847
1126
|
}
|
|
848
|
-
for (const a of ['page-size', 'size-param', 'cursor', 'direction-param', 'page-param']) {
|
|
1127
|
+
for (const a of ['page-size', 'size-param', 'cursor', 'direction-param', 'direction-values', 'page-param', 'offset-param']) {
|
|
849
1128
|
if (attr(el, a) != null && !paginate && mode == null)
|
|
850
1129
|
this.report(el, a, 'SN102', `\`${a}\` needs \`paginate\``, 0);
|
|
851
1130
|
}
|
|
@@ -862,13 +1141,16 @@ class Compiler {
|
|
|
862
1141
|
}
|
|
863
1142
|
const order = attr(el, 'order');
|
|
864
1143
|
const type = attr(el, 'type')?.trim() || null;
|
|
865
|
-
const openapi = type ? null : this.openapi(el, src, attr(el, 'items') ?? null, attr(el, 'total') ?? null);
|
|
1144
|
+
const openapi = type || hasCall ? null : this.openapi(el, src, attr(el, 'items') ?? null, attr(el, 'total') ?? null);
|
|
866
1145
|
if (this.check) {
|
|
867
1146
|
const r = { name, type, loc: this.locOf(el, type ? 'type' : null), nameLoc: this.locOf(el, 'name') };
|
|
868
1147
|
if (openapi && 'schema' in openapi)
|
|
869
1148
|
r.openapi = openapi;
|
|
1149
|
+
if (call)
|
|
1150
|
+
r.call = { name: call.name, items: attr(el, 'items') ?? null };
|
|
870
1151
|
this.check.resources.push(r);
|
|
871
|
-
|
|
1152
|
+
// a resource fed by an action is typed by the action's result
|
|
1153
|
+
if (!type && !r.openapi && !r.call) {
|
|
872
1154
|
const hint = openapi && 'closest' in openapi
|
|
873
1155
|
? `no OpenAPI GET operation matches \`src\`${openapi.closest ? ` (closest path: \`${openapi.closest}\`)` : ''}; add \`type="…"\`, or \`type="any"\` to leave it unchecked`
|
|
874
1156
|
: 'add `type="…"` with a type from the types module, or `type="any"` to leave it unchecked';
|
|
@@ -879,6 +1161,8 @@ class Compiler {
|
|
|
879
1161
|
name, src, key: info.key, items: attr(el, 'items') ?? null, total: attr(el, 'total') ?? null,
|
|
880
1162
|
order: order ? order.trim() : null, when, paginate, deps: [...deps].sort(), scope: ctx.stateIdx, data: null,
|
|
881
1163
|
...(cache !== undefined ? { cache } : {}),
|
|
1164
|
+
...(each ? { each } : {}),
|
|
1165
|
+
...(call ? { call: { args: call.args, run: call.run } } : {}),
|
|
882
1166
|
});
|
|
883
1167
|
}
|
|
884
1168
|
/** Matches a resource against the OpenAPI specs; checks `items`, `total` and query parameter names. */
|
|
@@ -940,7 +1224,7 @@ class Compiler {
|
|
|
940
1224
|
if (ev)
|
|
941
1225
|
events.push(ev);
|
|
942
1226
|
}
|
|
943
|
-
this.syncs.push({ src, events });
|
|
1227
|
+
this.syncs.push({ src, events, ...(ctx.stateIdx >= 0 ? { scope: ctx.stateIdx } : {}) });
|
|
944
1228
|
this.features.add('sse');
|
|
945
1229
|
}
|
|
946
1230
|
event(el, ctx) {
|
|
@@ -972,7 +1256,7 @@ class Compiler {
|
|
|
972
1256
|
const fnOf = (n, object = false) => {
|
|
973
1257
|
if (attr(el, n) == null)
|
|
974
1258
|
return null;
|
|
975
|
-
const r = this.expr(el, n, scope, { mode: 'value', payload: true });
|
|
1259
|
+
const r = this.expr(el, n, scope, { mode: 'value', payload: true, payloadState: true });
|
|
976
1260
|
this.typeLast({
|
|
977
1261
|
payload: payloadType,
|
|
978
1262
|
...(n === 'item' ? { returns: { kind: 'item', resource } } : n === 'set' ? { returns: { kind: 'item', resource, partial: true } } : {}),
|
|
@@ -1011,7 +1295,7 @@ class Compiler {
|
|
|
1011
1295
|
const usesEl = (r) => ({ ...(r.usesEl ? { e: 1 } : {}), ...(r.usesIndex ? { i: 1 } : {}) });
|
|
1012
1296
|
function binding(b) {
|
|
1013
1297
|
const out = { k: b.k };
|
|
1014
|
-
for (const key of ['f', 'd', 'n', 'x', 'a', 'y', 'w', 'p', 'e', 'i']) {
|
|
1298
|
+
for (const key of ['f', 'd', 'n', 'x', 'a', 'y', 'w', 'p', 'e', 'i', 'ms']) {
|
|
1015
1299
|
const v = b[key];
|
|
1016
1300
|
if (v === undefined || (Array.isArray(v) && v.length === 0 && key !== 'd'))
|
|
1017
1301
|
continue;
|
|
@@ -59,6 +59,11 @@ export const DIAGNOSTIC_DOCS = {
|
|
|
59
59
|
means: 'TypeScript found a type error in an expression. Reported by `sinter check`, `sinter({ typecheck: true })` and the editor integration.',
|
|
60
60
|
fix: 'Fix the expression, or the type: `type` on `<sn-resource>`, the OpenAPI spec, or a JSDoc cast in `data-sn-state`.',
|
|
61
61
|
},
|
|
62
|
+
SN209: {
|
|
63
|
+
example: '`s` is already defined here by `data-sn-let`; use another name',
|
|
64
|
+
means: 'A `data-sn-let` name is inlined wherever it is read, so it cannot share a name with a state variable or loop variable in its scope, or be redeclared inside it.',
|
|
65
|
+
fix: 'Rename the let, or the state or loop variable.',
|
|
66
|
+
},
|
|
62
67
|
SN301: {
|
|
63
68
|
example: 'resource `mesages` is not declared on this page',
|
|
64
69
|
means: 'A store call, `$meta` or embedded data (`data-sn-data`) names a resource that no `<sn-resource>` on the page or its partials declares.',
|
|
@@ -71,13 +76,13 @@ export const DIAGNOSTIC_DOCS = {
|
|
|
71
76
|
},
|
|
72
77
|
SN303: {
|
|
73
78
|
example: '`$meta.contacts.loadng` is not a status field. Did you mean `loading`?',
|
|
74
|
-
means: '`$meta.<resource>` has a fixed set of fields: `loading`, `error`, `loadedAt`, `pending`, `total`, and with `paginate` also `
|
|
79
|
+
means: '`$meta.<resource>` has a fixed set of fields: `loading`, `error`, `loadedAt`, `pending`, `total`, and with `paginate` also `pages`, `from`, `to`, `hasPrev`, `hasNext`; `page` with `paginate="offset"`, or keyset with `offset-param`.',
|
|
75
80
|
fix: 'Use the suggested field. A pagination field needs `paginate` on the resource.',
|
|
76
81
|
},
|
|
77
82
|
SN304: {
|
|
78
|
-
example: 'keyset pagination cannot jump to a page number.
|
|
79
|
-
means: 'Keyset (cursor) pagination only knows the next and previous page, so `page(\'name\', 7)` has nothing to jump to.',
|
|
80
|
-
fix: '
|
|
83
|
+
example: 'keyset pagination cannot jump to a page number. Add `offset-param` if the server also accepts an offset, or use `paginate="offset"`',
|
|
84
|
+
means: 'Keyset (cursor) pagination only knows the next and previous page, so `page(\'name\', 7)` has nothing to jump to, unless the server can also skip items by offset.',
|
|
85
|
+
fix: 'If the API accepts an offset next to its cursor, add `offset-param="offset"`: the jump is sent as an offset and `prev`/`next` continue by cursor. Otherwise use `paginate="offset"`, or only step with `first`, `prev`, `next` and `last`.',
|
|
81
86
|
},
|
|
82
87
|
SN305: {
|
|
83
88
|
example: 'partial `./partials/contact-dialg.html` not found or outside the project root',
|
|
@@ -99,6 +104,16 @@ export const DIAGNOSTIC_DOCS = {
|
|
|
99
104
|
means: 'The resource matched an operation in the OpenAPI spec, but its `items` or `total` path is not in the response schema.',
|
|
100
105
|
fix: 'Fix the path in `items` or `total`, or the spec if the API changed.',
|
|
101
106
|
},
|
|
107
|
+
SN309: {
|
|
108
|
+
example: 'no `<dialog id="compose-dialg">` on this page. Did you mean `compose-dialog`?',
|
|
109
|
+
means: '`showModal(id)` and `close(id)` name a `<dialog>` by its literal `id`, which must be on the page or one of its partials.',
|
|
110
|
+
fix: 'Fix the id, or add `id` to the `<dialog>`. `close()` without an argument closes the dialog around the element.',
|
|
111
|
+
},
|
|
112
|
+
SN310: {
|
|
113
|
+
example: '`pgp.decrpyt` is not exported by the actions module. Did you mean `pgp.decrypt`?',
|
|
114
|
+
means: '`data-sn-call` and `<sn-resource call>` name a function the actions module (`src/sinter.actions.js` or `.ts`) exports, directly or through `export * as ns`.',
|
|
115
|
+
fix: 'Use the suggested name, or export the function from the actions module.',
|
|
116
|
+
},
|
|
102
117
|
SN401: {
|
|
103
118
|
example: '`data-sn-for` takes `item in list` or `(item, i) in list`',
|
|
104
119
|
means: 'An attribute value has the wrong form: a JavaScript syntax error (the column points inside the attribute), or a value outside what the attribute accepts, such as `paginate` or `data-sn-concurrency`.',
|
|
@@ -122,7 +137,7 @@ export const DIAGNOSTIC_DOCS = {
|
|
|
122
137
|
SN504: {
|
|
123
138
|
example: 'resource `contacts` has no type: expressions using it are not checked',
|
|
124
139
|
means: 'The type checker has no type for the resource, so expressions that use it are not checked. Reported by `sinter check` only.',
|
|
125
|
-
fix: 'Add `type="Contact[]"` with a type from `src/sinter.types.ts`, configure an OpenAPI spec that matches `src`, or write `type="any"` to opt out.',
|
|
140
|
+
fix: 'Add `type="Contact[]"` with a type from `src/sinter.types.ts`, configure an OpenAPI spec that matches `src`, load it with `call` (typed by what the action returns), or write `type="any"` to opt out.',
|
|
126
141
|
},
|
|
127
142
|
SN505: {
|
|
128
143
|
example: '`GET /articles` declares no query parameter `tagg`. Did you mean `tag`?',
|