@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/runtime/store.js
CHANGED
|
@@ -12,13 +12,27 @@ export function clean(obj) {
|
|
|
12
12
|
out[k] = obj[k];
|
|
13
13
|
return out;
|
|
14
14
|
}
|
|
15
|
+
/** One collator per locale: constructing one is costly in some engines. */
|
|
16
|
+
const collators = new Map();
|
|
17
|
+
const collator = (l = '') => collators.get(l) ?? (collators.set(l, new Intl.Collator(l || undefined, { numeric: true })), collators.get(l));
|
|
15
18
|
export function createResource(name, o) {
|
|
16
19
|
const meta = { loading: false, error: null, loadedAt: null, pending: 0, total: null };
|
|
17
20
|
const coll = !!o.key, field = o.order?.replace(/^-/, ''), dir = o.order?.[0] == '-' ? -1 : 1;
|
|
18
21
|
const subs = new Set();
|
|
19
22
|
const prep = (x) => (o.prep ? o.prep(x) : x);
|
|
20
|
-
|
|
23
|
+
/** base items in arrival order, and each key's arrival number (ties in `order` keep arrival order) */
|
|
24
|
+
let base = new Map(), seq = new Map(), next = 0;
|
|
25
|
+
let single = null, total = null;
|
|
21
26
|
let layers = [], lastLayer = 0;
|
|
27
|
+
/**
|
|
28
|
+
* The base in display order, the locale it was sorted for, and the keys written since, with
|
|
29
|
+
* their value before the first write. Reads patch it instead of sorting
|
|
30
|
+
* again. Arrays handed out are never changed afterwards.
|
|
31
|
+
*/
|
|
32
|
+
let sorted = null, sortedFor, touched = new Map();
|
|
33
|
+
/** Optimistic layers applied over the base: changed keys (null: removed), keys they add (arrival numbers), removals counted for `total`. */
|
|
34
|
+
let over = null;
|
|
35
|
+
/** what readers see; rebuilt on the first read after a change */
|
|
22
36
|
let view = null;
|
|
23
37
|
let journal = null, loads = 0;
|
|
24
38
|
const keyOf = (x) => typeof x == 'string' ? x : (o.key ?? []).map((f) => {
|
|
@@ -29,71 +43,171 @@ export function createResource(name, o) {
|
|
|
29
43
|
const merge = (cur, op) => op.op == 'upsert' ? prep(op.item)
|
|
30
44
|
: op.op == 'remove' ? null
|
|
31
45
|
: cur && prep({ ...cur, ...clean(op.fields) });
|
|
32
|
-
/** Applies one op to
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
/** Applies one op to the base. */
|
|
47
|
+
const apply = (op) => {
|
|
48
|
+
if (!coll)
|
|
49
|
+
return void (single = merge(single, op));
|
|
50
|
+
const k = op.op == 'upsert' ? keyOf(op.item) : op.key, cur = base.get(k), v = merge(cur, op);
|
|
51
|
+
if (sorted && !touched.has(k))
|
|
52
|
+
touched.set(k, cur);
|
|
53
|
+
if (v) {
|
|
54
|
+
if (!cur)
|
|
55
|
+
seq.set(k, next++);
|
|
56
|
+
base.set(k, v);
|
|
57
|
+
}
|
|
58
|
+
else if (cur) {
|
|
59
|
+
base.delete(k);
|
|
60
|
+
seq.delete(k);
|
|
61
|
+
}
|
|
39
62
|
};
|
|
40
|
-
|
|
63
|
+
/** By `order` (none: all equal, so arrival order decides). */
|
|
64
|
+
const compare = (a, b) => {
|
|
65
|
+
if (!field)
|
|
66
|
+
return 0;
|
|
67
|
+
const x = a[field], y = b[field];
|
|
68
|
+
if (x == null || y == null)
|
|
69
|
+
return +(x == null) - +(y == null);
|
|
70
|
+
return dir * (typeof x == 'string' && typeof y == 'string' ? collator(sortedFor).compare(x, y) : +(x > y) - +(x < y));
|
|
71
|
+
};
|
|
72
|
+
/** A sorted copy of `list` without the `drop` items and with the `add` items (each with its arrival number). */
|
|
73
|
+
function patch(list, drop, add, arrival) {
|
|
74
|
+
const out = list.slice();
|
|
75
|
+
/** the first index whose item does not sort before `v` (with arrival number `n`, after its ties) */
|
|
76
|
+
const find = (v, n) => {
|
|
77
|
+
let lo = 0, hi = out.length;
|
|
78
|
+
while (lo < hi) {
|
|
79
|
+
const mid = (lo + hi) >> 1, c = compare(out[mid], v) || (n == null ? 0 : arrival(keyOf(out[mid])) - n);
|
|
80
|
+
c < 0 ? (lo = mid + 1) : (hi = mid);
|
|
81
|
+
}
|
|
82
|
+
return lo;
|
|
83
|
+
};
|
|
84
|
+
for (const v of drop) {
|
|
85
|
+
// among its ties, by identity; an item whose order field was changed in place is found by a scan
|
|
86
|
+
let i = find(v);
|
|
87
|
+
while (out[i] !== v && i < out.length && !compare(out[i], v))
|
|
88
|
+
i++;
|
|
89
|
+
out.splice(out[i] === v ? i : out.indexOf(v), 1);
|
|
90
|
+
}
|
|
91
|
+
for (const [v, n] of add)
|
|
92
|
+
out.splice(find(v, n), 0, v);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** The base in display order. */
|
|
96
|
+
function baseList() {
|
|
97
|
+
const locale = o.locale?.();
|
|
98
|
+
if (sorted && locale === sortedFor && touched.size <= 64) {
|
|
99
|
+
if (touched.size) {
|
|
100
|
+
const drop = [], add = [];
|
|
101
|
+
for (const [k, old] of touched) {
|
|
102
|
+
if (old)
|
|
103
|
+
drop.push(old);
|
|
104
|
+
const v = base.get(k);
|
|
105
|
+
if (v)
|
|
106
|
+
add.push([v, seq.get(k)]);
|
|
107
|
+
}
|
|
108
|
+
sorted = patch(sorted, drop, add, (k) => seq.get(k));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
sortedFor = locale;
|
|
113
|
+
// a stable sort of the Map's arrival order
|
|
114
|
+
sorted = [...base.values()].sort(compare);
|
|
115
|
+
}
|
|
116
|
+
touched = new Map();
|
|
117
|
+
return sorted;
|
|
118
|
+
}
|
|
119
|
+
/** Recomputes the layers' effect and `meta.total` after any change; marks `meta` if the total changed. */
|
|
120
|
+
function sync() {
|
|
121
|
+
over = null;
|
|
122
|
+
if (layers.length) {
|
|
123
|
+
const m = new Map(), add = new Map();
|
|
124
|
+
let removed = 0, n = next, s = single;
|
|
125
|
+
for (const l of layers)
|
|
126
|
+
for (const op of l.ops) {
|
|
127
|
+
if (!coll) {
|
|
128
|
+
s = merge(s, op);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const k = op.op == 'upsert' ? keyOf(op.item) : op.key, cur = m.has(k) ? m.get(k) : base.get(k), v = merge(cur, op);
|
|
132
|
+
if (v) {
|
|
133
|
+
if (!cur)
|
|
134
|
+
add.set(k, n++);
|
|
135
|
+
m.set(k, v);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
if (cur)
|
|
139
|
+
removed++;
|
|
140
|
+
m.set(k, null);
|
|
141
|
+
add.delete(k);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
over = { m, add, removed, s };
|
|
145
|
+
}
|
|
146
|
+
const t = total == null ? null : Math.max(0, total - (over?.removed ?? 0));
|
|
147
|
+
if (meta.total !== t) {
|
|
148
|
+
meta.total = t;
|
|
149
|
+
r.onDirty('meta');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** A data change: readers rebuild their view; `meta` too when the pending count changed. */
|
|
153
|
+
const changed = (pending) => {
|
|
41
154
|
view = null;
|
|
155
|
+
sync();
|
|
42
156
|
r.onDirty('data');
|
|
43
|
-
if (
|
|
157
|
+
if (pending) {
|
|
158
|
+
meta.pending = layers.length;
|
|
44
159
|
r.onDirty('meta');
|
|
160
|
+
}
|
|
45
161
|
};
|
|
46
162
|
function visible() {
|
|
47
163
|
if (view)
|
|
48
164
|
return view;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return dir * (typeof x == 'string' && typeof y == 'string' ? c.compare(x, y) : +(x > y) - +(x < y));
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
view = { l: Object.freeze(l), m, s };
|
|
67
|
-
if (meta.total !== t) {
|
|
68
|
-
meta.total = t;
|
|
69
|
-
r.onDirty('meta');
|
|
165
|
+
if (!coll)
|
|
166
|
+
return (view = { l: [], get: () => undefined, s: over ? over.s : single });
|
|
167
|
+
let l = baseList(), get = (k) => base.get(k);
|
|
168
|
+
if (over) {
|
|
169
|
+
const { m, add } = over, drop = [], put = [], at = (k) => add.get(k) ?? seq.get(k);
|
|
170
|
+
for (const [k, v] of m) {
|
|
171
|
+
const b = base.get(k);
|
|
172
|
+
if (b)
|
|
173
|
+
drop.push(b);
|
|
174
|
+
if (v)
|
|
175
|
+
put.push([v, at(k)]);
|
|
176
|
+
}
|
|
177
|
+
l = patch(l, drop, put, at);
|
|
178
|
+
get = (k) => (m.has(k) ? m.get(k) ?? undefined : base.get(k));
|
|
70
179
|
}
|
|
71
|
-
|
|
180
|
+
// dev builds freeze what expressions see; the compiler refuses writes to resources (SN205)
|
|
181
|
+
return (view = { l: o.freeze ? Object.freeze(l.slice()) : l, get, s: null });
|
|
72
182
|
}
|
|
73
183
|
function write(op) {
|
|
74
184
|
if (r.recording)
|
|
75
185
|
return void r.recording.push(op);
|
|
76
186
|
journal?.push(op);
|
|
77
|
-
|
|
187
|
+
apply(op);
|
|
78
188
|
changed();
|
|
79
189
|
for (const f of subs)
|
|
80
190
|
f({ resource: name, ...op });
|
|
81
191
|
}
|
|
82
192
|
const r = {
|
|
83
193
|
name, meta, key: o.key, isCollection: coll, keyOf, recording: null, loader: null, onDirty() { },
|
|
84
|
-
get: (k) => (coll ? visible().
|
|
194
|
+
get: (k) => (coll ? visible().get(keyOf(k)) : visible().s ?? undefined),
|
|
85
195
|
has: (k) => r.get(k) !== undefined,
|
|
86
196
|
all: () => visible().l,
|
|
87
197
|
value: () => (coll ? visible().l : visible().s),
|
|
88
198
|
replace(items) {
|
|
89
199
|
if (coll) {
|
|
90
200
|
base = new Map();
|
|
201
|
+
seq = new Map();
|
|
202
|
+
sorted = null;
|
|
203
|
+
touched = new Map();
|
|
91
204
|
for (const x of Array.isArray(items) ? items : []) {
|
|
92
205
|
const k = keyOf(x);
|
|
93
206
|
if (base.has(k))
|
|
94
207
|
o.warn?.(13, name, k);
|
|
95
208
|
base.delete(k);
|
|
96
209
|
base.set(k, prep(x));
|
|
210
|
+
seq.set(k, next++);
|
|
97
211
|
}
|
|
98
212
|
}
|
|
99
213
|
else
|
|
@@ -105,7 +219,7 @@ export function createResource(name, o) {
|
|
|
105
219
|
upsert: (item) => write({ op: 'upsert', item }),
|
|
106
220
|
patch: (k, fields) => write({ op: 'patch', key: keyOf(k), fields }),
|
|
107
221
|
remove: (k) => write({ op: 'remove', key: keyOf(k) }),
|
|
108
|
-
refetch: () => r.loader?.refetch() ?? Promise.resolve(),
|
|
222
|
+
refetch: (hint) => r.loader?.refetch(hint) ?? Promise.resolve(),
|
|
109
223
|
subscribe: (fn) => (subs.add(fn), () => subs.delete(fn)),
|
|
110
224
|
startLoad() {
|
|
111
225
|
loads++;
|
|
@@ -119,23 +233,22 @@ export function createResource(name, o) {
|
|
|
119
233
|
if (!loaded || !ops.length)
|
|
120
234
|
return;
|
|
121
235
|
for (const op of ops)
|
|
122
|
-
|
|
236
|
+
apply(op);
|
|
123
237
|
changed();
|
|
124
238
|
},
|
|
125
239
|
addLayer(ops) {
|
|
126
240
|
layers.push({ id: ++lastLayer, ops });
|
|
127
|
-
|
|
128
|
-
changed('meta');
|
|
241
|
+
changed(true);
|
|
129
242
|
return lastLayer;
|
|
130
243
|
},
|
|
131
244
|
dropLayer(id) {
|
|
132
245
|
const n = layers.length;
|
|
133
246
|
layers = layers.filter((l) => l.id != id);
|
|
134
|
-
meta.pending = layers.length;
|
|
135
247
|
if (n != layers.length)
|
|
136
|
-
changed(
|
|
248
|
+
changed(true);
|
|
137
249
|
},
|
|
138
|
-
|
|
250
|
+
/** the total changes `meta` only: bindings that read the items do not re-run */
|
|
251
|
+
setTotal(t) { total = t; sync(); },
|
|
139
252
|
base: () => ({ items: coll ? [...base.values()] : single, total }),
|
|
140
253
|
};
|
|
141
254
|
return r;
|
package/dist/sse.js
CHANGED
|
@@ -45,10 +45,13 @@ class Connection {
|
|
|
45
45
|
everOpen = false;
|
|
46
46
|
url;
|
|
47
47
|
fed;
|
|
48
|
+
/** where `src` and the events' expressions are evaluated: the enclosing state's scope, or the page */
|
|
49
|
+
base;
|
|
48
50
|
constructor(app, def) {
|
|
49
51
|
this.app = app;
|
|
50
52
|
this.def = def;
|
|
51
|
-
this.
|
|
53
|
+
this.base = app.scopeOf(def.scope ?? -1);
|
|
54
|
+
this.url = app.url(def.src, this.base);
|
|
52
55
|
this.fed = [...new Set(def.events.map((e) => e.resource))];
|
|
53
56
|
}
|
|
54
57
|
start() {
|
|
@@ -116,7 +119,7 @@ class Connection {
|
|
|
116
119
|
status = (await this.app.req.raw(config.sessionProbe)).status;
|
|
117
120
|
}
|
|
118
121
|
catch (e) {
|
|
119
|
-
status = e instanceof SinterError ? e.status : 0;
|
|
122
|
+
status = e instanceof SinterError ? e.status ?? 0 : 0;
|
|
120
123
|
}
|
|
121
124
|
if (this.closed || this.hidden)
|
|
122
125
|
return;
|
|
@@ -139,7 +142,7 @@ class Connection {
|
|
|
139
142
|
}
|
|
140
143
|
apply(def, payload) {
|
|
141
144
|
const app = this.app;
|
|
142
|
-
const scope = Object.create(
|
|
145
|
+
const scope = Object.create(this.base);
|
|
143
146
|
scope.$p = payload && typeof payload === 'object' ? payload : {};
|
|
144
147
|
const r = app.resource(def.resource);
|
|
145
148
|
try {
|
|
@@ -148,13 +151,13 @@ class Connection {
|
|
|
148
151
|
const paginated = !!app.ir.resources.find((x) => x.name === def.resource)?.paginate;
|
|
149
152
|
switch (def.op) {
|
|
150
153
|
case 'refetch':
|
|
151
|
-
void r.refetch();
|
|
154
|
+
void r.refetch(scope.$p);
|
|
152
155
|
return;
|
|
153
156
|
case 'upsert': {
|
|
154
157
|
const item = def.item != null ? app.fns[def.item](scope) : payload;
|
|
155
158
|
if (item == null || typeof item !== 'object') {
|
|
156
159
|
if (def.fallback === 'refetch')
|
|
157
|
-
void r.refetch();
|
|
160
|
+
void r.refetch(scope.$p);
|
|
158
161
|
return;
|
|
159
162
|
}
|
|
160
163
|
if (paginated && !r.has(item)) {
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface RenderOptions {
|
|
|
44
44
|
now?: number | Date;
|
|
45
45
|
/** helper functions, and namespaces as objects of functions */
|
|
46
46
|
helpers?: Record<string, unknown>;
|
|
47
|
+
/** action functions for `data-sn-call` and `<sn-resource call>`, and namespaces as objects of functions */
|
|
48
|
+
actions?: Record<string, unknown>;
|
|
47
49
|
/** partials for `<sn-include>`, by path relative to the page */
|
|
48
50
|
files?: Record<string, string>;
|
|
49
51
|
/** localStorage and sessionStorage (default: empty ones); pass the previous page's to test what carries over */
|
package/dist/testing/index.js
CHANGED
|
@@ -11,9 +11,11 @@ import cache from '../cache.js';
|
|
|
11
11
|
import dev from '../dev.js';
|
|
12
12
|
import optimistic from '../optimistic.js';
|
|
13
13
|
import paginate from '../paginate.js';
|
|
14
|
+
import each from '../each.js';
|
|
14
15
|
import persist from '../persist.js';
|
|
16
|
+
import listbox from '../listbox.js';
|
|
15
17
|
import sse from '../sse.js';
|
|
16
|
-
const FEATURES = { dev, sse, optimistic, paginate, persist, cache };
|
|
18
|
+
const FEATURES = { dev, sse, optimistic, paginate, persist, cache, each, listbox };
|
|
17
19
|
function isResponseSpec(v) {
|
|
18
20
|
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
19
21
|
return false;
|
|
@@ -172,9 +174,17 @@ export async function render(html, options = {}) {
|
|
|
172
174
|
else if (v && typeof v === 'object')
|
|
173
175
|
manifest.namespaces[k] = { module: k, members: Object.keys(v) };
|
|
174
176
|
}
|
|
177
|
+
const actions = options.actions ?? {};
|
|
178
|
+
const actionManifest = { module: 'actions', names: [], namespaces: {} };
|
|
179
|
+
for (const [k, v] of Object.entries(actions)) {
|
|
180
|
+
if (typeof v === 'function')
|
|
181
|
+
actionManifest.names.push(k);
|
|
182
|
+
else if (v && typeof v === 'object')
|
|
183
|
+
actionManifest.namespaces[k] = { module: k, members: Object.keys(v) };
|
|
184
|
+
}
|
|
175
185
|
const files = options.files ?? {};
|
|
176
186
|
const parts = compileParts(html, {
|
|
177
|
-
file: PAGE, root: '/test', dev: true, helpers: manifest, moduleSrc: 'about:blank',
|
|
187
|
+
file: PAGE, root: '/test', dev: true, helpers: manifest, actions: actionManifest, moduleSrc: 'about:blank',
|
|
178
188
|
readFile: (p) => {
|
|
179
189
|
const rel = p.startsWith('/test/') ? p.slice('/test/'.length) : p;
|
|
180
190
|
const hit = files[rel] ?? files[`./${rel}`] ?? files[p];
|
|
@@ -203,6 +213,11 @@ export async function render(html, options = {}) {
|
|
|
203
213
|
values.push(helpers[ns][m]);
|
|
204
214
|
}
|
|
205
215
|
}
|
|
216
|
+
for (const c of parts.calls) {
|
|
217
|
+
const [a, b] = c.split('.');
|
|
218
|
+
names.push(`$a$${c.replace('.', '$')}`);
|
|
219
|
+
values.push(b === undefined ? actions[a] : actions[a][b]);
|
|
220
|
+
}
|
|
206
221
|
// eslint-disable-next-line no-new-func -- tests only; pages load a compiled module
|
|
207
222
|
const fns = new Function(...names, `"use strict"; return [\n${parts.fns.join(',\n')}\n];`)(...values);
|
|
208
223
|
// the page
|
package/dist/vite.js
CHANGED
|
@@ -51,7 +51,7 @@ export default function sinter(options = {}) {
|
|
|
51
51
|
return;
|
|
52
52
|
const { check } = await import('./check/index.js');
|
|
53
53
|
const p = project(root, options);
|
|
54
|
-
const r = check({ root, units: [...units.values()], helpers: p.helpers, types: p.types, openapi: p.openapi });
|
|
54
|
+
const r = check({ root, units: [...units.values()], helpers: p.helpers, actions: p.actions, types: p.types, openapi: p.openapi });
|
|
55
55
|
units.clear();
|
|
56
56
|
if (r.diagnostics.length)
|
|
57
57
|
this.error(`sinter check: ${r.diagnostics.length} type error(s)\n${r.diagnostics.map(formatDiagnostic).join('\n')}`);
|
|
@@ -66,7 +66,7 @@ export default function sinter(options = {}) {
|
|
|
66
66
|
if (p.diagnostics.length)
|
|
67
67
|
throw new Error(`sinter: ${p.diagnostics.map(formatDiagnostic).join('\n')}`);
|
|
68
68
|
const out = compile(html, {
|
|
69
|
-
file, root, dev, helpers: p.helpers, moduleSrc, config: p.config, readFile: p.readFile,
|
|
69
|
+
file, root, dev, helpers: p.helpers, actions: p.actions, moduleSrc, config: p.config, readFile: p.readFile,
|
|
70
70
|
check: !!options.typecheck && !dev, openapi: p.openapi,
|
|
71
71
|
});
|
|
72
72
|
if (out.check)
|
|
@@ -75,6 +75,8 @@ export default function sinter(options = {}) {
|
|
|
75
75
|
watch(f);
|
|
76
76
|
if (p.helpersFile)
|
|
77
77
|
watch(p.helpersFile);
|
|
78
|
+
if (p.actionsFile)
|
|
79
|
+
watch(p.actionsFile);
|
|
78
80
|
const errors = out.diagnostics.filter((d) => d.severity === 'error');
|
|
79
81
|
for (const d of out.diagnostics)
|
|
80
82
|
if (d.severity === 'warning')
|
package/docs/diagnostics.md
CHANGED
|
@@ -122,6 +122,16 @@ TypeScript found a type error in an expression. Reported by `sinter check`, `sin
|
|
|
122
122
|
|
|
123
123
|
**Fix:** Fix the expression, or the type: `type` on `<sn-resource>`, the OpenAPI spec, or a JSDoc cast in `data-sn-state`.
|
|
124
124
|
|
|
125
|
+
## SN209 name-conflict
|
|
126
|
+
|
|
127
|
+
Error, for example:
|
|
128
|
+
|
|
129
|
+
> `s` is already defined here by `data-sn-let`; use another name
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
133
|
+
**Fix:** Rename the let, or the state or loop variable.
|
|
134
|
+
|
|
125
135
|
## SN301 unknown-resource
|
|
126
136
|
|
|
127
137
|
Error, for example:
|
|
@@ -148,7 +158,7 @@ Error, for example:
|
|
|
148
158
|
|
|
149
159
|
> `$meta.contacts.loadng` is not a status field. Did you mean `loading`?
|
|
150
160
|
|
|
151
|
-
`$meta.<resource>` has a fixed set of fields: `loading`, `error`, `loadedAt`, `pending`, `total`, and with `paginate` also `
|
|
161
|
+
`$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`.
|
|
152
162
|
|
|
153
163
|
**Fix:** Use the suggested field. A pagination field needs `paginate` on the resource.
|
|
154
164
|
|
|
@@ -156,11 +166,11 @@ Error, for example:
|
|
|
156
166
|
|
|
157
167
|
Error, for example:
|
|
158
168
|
|
|
159
|
-
> keyset pagination cannot jump to a page number.
|
|
169
|
+
> keyset pagination cannot jump to a page number. Add `offset-param` if the server also accepts an offset, or use `paginate="offset"`
|
|
160
170
|
|
|
161
|
-
Keyset (cursor) pagination only knows the next and previous page, so `page('name', 7)` has nothing to jump to.
|
|
171
|
+
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.
|
|
162
172
|
|
|
163
|
-
**Fix:**
|
|
173
|
+
**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`.
|
|
164
174
|
|
|
165
175
|
## SN305 include-not-found
|
|
166
176
|
|
|
@@ -202,6 +212,26 @@ The resource matched an operation in the OpenAPI spec, but its `items` or `total
|
|
|
202
212
|
|
|
203
213
|
**Fix:** Fix the path in `items` or `total`, or the spec if the API changed.
|
|
204
214
|
|
|
215
|
+
## SN309 unknown-dialog
|
|
216
|
+
|
|
217
|
+
Error, for example:
|
|
218
|
+
|
|
219
|
+
> no `<dialog id="compose-dialg">` on this page. Did you mean `compose-dialog`?
|
|
220
|
+
|
|
221
|
+
`showModal(id)` and `close(id)` name a `<dialog>` by its literal `id`, which must be on the page or one of its partials.
|
|
222
|
+
|
|
223
|
+
**Fix:** Fix the id, or add `id` to the `<dialog>`. `close()` without an argument closes the dialog around the element.
|
|
224
|
+
|
|
225
|
+
## SN310 unknown-action
|
|
226
|
+
|
|
227
|
+
Error, for example:
|
|
228
|
+
|
|
229
|
+
> `pgp.decrpyt` is not exported by the actions module. Did you mean `pgp.decrypt`?
|
|
230
|
+
|
|
231
|
+
`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`.
|
|
232
|
+
|
|
233
|
+
**Fix:** Use the suggested name, or export the function from the actions module.
|
|
234
|
+
|
|
205
235
|
## SN401 parse-error
|
|
206
236
|
|
|
207
237
|
Error, for example:
|
|
@@ -250,7 +280,7 @@ Warning, for example:
|
|
|
250
280
|
|
|
251
281
|
The type checker has no type for the resource, so expressions that use it are not checked. Reported by `sinter check` only.
|
|
252
282
|
|
|
253
|
-
**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.
|
|
283
|
+
**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.
|
|
254
284
|
|
|
255
285
|
## SN505 unknown-query-param
|
|
256
286
|
|
package/html-custom-data.json
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
{
|
|
12
12
|
"name": "src"
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
"name": "call"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "each"
|
|
19
|
+
},
|
|
14
20
|
{
|
|
15
21
|
"name": "key"
|
|
16
22
|
},
|
|
@@ -41,9 +47,15 @@
|
|
|
41
47
|
{
|
|
42
48
|
"name": "direction-param"
|
|
43
49
|
},
|
|
50
|
+
{
|
|
51
|
+
"name": "direction-values"
|
|
52
|
+
},
|
|
44
53
|
{
|
|
45
54
|
"name": "page-param"
|
|
46
55
|
},
|
|
56
|
+
{
|
|
57
|
+
"name": "offset-param"
|
|
58
|
+
},
|
|
47
59
|
{
|
|
48
60
|
"name": "type"
|
|
49
61
|
},
|
|
@@ -106,6 +118,10 @@
|
|
|
106
118
|
"name": "data-sn-state",
|
|
107
119
|
"description": "declares local reactive state for the element and its subtree (object literal)"
|
|
108
120
|
},
|
|
121
|
+
{
|
|
122
|
+
"name": "data-sn-let",
|
|
123
|
+
"description": "derived names for the element and its subtree, computed where they are read; later names may use earlier ones (object literal {name: expression})"
|
|
124
|
+
},
|
|
109
125
|
{
|
|
110
126
|
"name": "data-sn-text",
|
|
111
127
|
"description": "sets textContent; never HTML (value expression)"
|
|
@@ -150,6 +166,10 @@
|
|
|
150
166
|
"name": "data-sn-else",
|
|
151
167
|
"description": "on a <template> directly after an if template"
|
|
152
168
|
},
|
|
169
|
+
{
|
|
170
|
+
"name": "data-sn-listbox",
|
|
171
|
+
"description": "on a text input: arrow keys mark a [role=option] of that list (empty: the nearest [role=listbox] around the input), Enter or Tab clicks it, Escape fires `dismiss` (id of the suggestion list, or empty)"
|
|
172
|
+
},
|
|
153
173
|
{
|
|
154
174
|
"name": "data-sn-persist",
|
|
155
175
|
"description": "persists the model or state in localStorage under sn: (storage key)"
|
|
@@ -174,6 +194,10 @@
|
|
|
174
194
|
"name": "data-sn-delete",
|
|
175
195
|
"description": "DELETE action (URL template)"
|
|
176
196
|
},
|
|
197
|
+
{
|
|
198
|
+
"name": "data-sn-call",
|
|
199
|
+
"description": "local action: calls a function from the actions module, e.g. pgp.decrypt(msg) (action call)"
|
|
200
|
+
},
|
|
177
201
|
{
|
|
178
202
|
"name": "data-sn-body",
|
|
179
203
|
"description": "JSON request body; overrides form serialization (value expression)"
|
|
@@ -184,7 +208,7 @@
|
|
|
184
208
|
},
|
|
185
209
|
{
|
|
186
210
|
"name": "data-sn-debounce",
|
|
187
|
-
"description": "delays the trigger, resets on each event (milliseconds)"
|
|
211
|
+
"description": "delays the trigger, resets on each event; without a method, delays the element's data-sn-on-* handlers (milliseconds)"
|
|
188
212
|
},
|
|
189
213
|
{
|
|
190
214
|
"name": "data-sn-concurrency",
|
|
@@ -200,7 +224,15 @@
|
|
|
200
224
|
},
|
|
201
225
|
{
|
|
202
226
|
"name": "data-sn-catch",
|
|
203
|
-
"description": "runs after an error; $error = {status, body, message} (handler expression)"
|
|
227
|
+
"description": "runs after an error; $error = {status, body, message, detail} (handler expression)"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "data-sn-confirm",
|
|
231
|
+
"description": "asks this question before the action runs (configure({ confirm }), window.confirm by default); no question when falsy (value expression)"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "data-sn-encoding",
|
|
235
|
+
"description": "form: the body (data-sn-body, arrays as repeated keys, or the form) goes as application/x-www-form-urlencoded (json | form)"
|
|
204
236
|
},
|
|
205
237
|
{
|
|
206
238
|
"name": "data-sn-path",
|
|
@@ -229,6 +261,18 @@
|
|
|
229
261
|
{
|
|
230
262
|
"name": "data-sn-on-keydown",
|
|
231
263
|
"description": "plain event handler, no request; e.g. data-sn-on-click"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "data-sn-debounce-input",
|
|
267
|
+
"description": "delays only that event's data-sn-on-<event> handler (it runs with the last event); e.g. data-sn-debounce-input"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"name": "data-sn-debounce-keyup",
|
|
271
|
+
"description": "delays only that event's data-sn-on-<event> handler (it runs with the last event); e.g. data-sn-debounce-input"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"name": "data-sn-debounce-scroll",
|
|
275
|
+
"description": "delays only that event's data-sn-on-<event> handler (it runs with the last event); e.g. data-sn-debounce-input"
|
|
232
276
|
}
|
|
233
277
|
]
|
|
234
278
|
}
|