@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.
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/dist/cache.d.ts +5 -0
- package/dist/cache.js +95 -0
- package/dist/check/index.d.ts +82 -0
- package/dist/check/index.js +156 -0
- package/dist/check/openapi-types.d.ts +6 -0
- package/dist/check/openapi-types.js +84 -0
- package/dist/check/shadow.d.ts +47 -0
- package/dist/check/shadow.js +245 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +67 -0
- package/dist/compiler/check-unit.d.ts +97 -0
- package/dist/compiler/check-unit.js +3 -0
- package/dist/compiler/compile.d.ts +62 -0
- package/dist/compiler/compile.js +1133 -0
- package/dist/compiler/diagnostic-docs.d.ts +10 -0
- package/dist/compiler/diagnostic-docs.js +137 -0
- package/dist/compiler/diagnostics.d.ts +56 -0
- package/dist/compiler/diagnostics.js +76 -0
- package/dist/compiler/expr.d.ts +89 -0
- package/dist/compiler/expr.js +838 -0
- package/dist/compiler/index.d.ts +21 -0
- package/dist/compiler/index.js +106 -0
- package/dist/compiler/openapi.d.ts +40 -0
- package/dist/compiler/openapi.js +177 -0
- package/dist/compiler/vocabulary.d.ts +12 -0
- package/dist/compiler/vocabulary.js +38 -0
- package/dist/dev.d.ts +10 -0
- package/dist/dev.js +92 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/ir.d.ts +127 -0
- package/dist/ir.js +121 -0
- package/dist/lsp/quickfix.d.ts +15 -0
- package/dist/lsp/quickfix.js +58 -0
- package/dist/lsp/rpc.d.ts +31 -0
- package/dist/lsp/rpc.js +120 -0
- package/dist/lsp/server.d.ts +36 -0
- package/dist/lsp/server.js +281 -0
- package/dist/lsp/spec-location.d.ts +9 -0
- package/dist/lsp/spec-location.js +42 -0
- package/dist/lsp/typescript.d.ts +77 -0
- package/dist/lsp/typescript.js +242 -0
- package/dist/lsp/workspace.d.ts +56 -0
- package/dist/lsp/workspace.js +227 -0
- package/dist/optimistic.d.ts +3 -0
- package/dist/optimistic.js +15 -0
- package/dist/paginate.d.ts +3 -0
- package/dist/paginate.js +184 -0
- package/dist/persist.d.ts +3 -0
- package/dist/persist.js +55 -0
- package/dist/project.d.ts +36 -0
- package/dist/project.js +100 -0
- package/dist/runtime/app.d.ts +125 -0
- package/dist/runtime/app.js +1183 -0
- package/dist/runtime/config.d.ts +53 -0
- package/dist/runtime/config.js +118 -0
- package/dist/runtime/env.d.ts +18 -0
- package/dist/runtime/env.js +23 -0
- package/dist/runtime/helpers.d.ts +9 -0
- package/dist/runtime/helpers.js +55 -0
- package/dist/runtime/request.d.ts +23 -0
- package/dist/runtime/request.js +93 -0
- package/dist/runtime/security.d.ts +12 -0
- package/dist/runtime/security.js +34 -0
- package/dist/runtime/store.d.ts +95 -0
- package/dist/runtime/store.js +142 -0
- package/dist/sse.d.ts +7 -0
- package/dist/sse.js +206 -0
- package/dist/testing/index.d.ts +71 -0
- package/dist/testing/index.js +345 -0
- package/dist/vite.d.ts +11 -0
- package/dist/vite.js +94 -0
- package/dist/ws.d.ts +18 -0
- package/dist/ws.js +57 -0
- package/docs/diagnostics.md +273 -0
- package/html-custom-data.json +234 -0
- package/llms.txt +531 -0
- package/package.json +90 -4
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Shadow files: one checked JavaScript file per page, in which every expression of
|
|
2
|
+
// the page appears verbatim, inside nested blocks that declare what it can see.
|
|
3
|
+
// TypeScript checks the file (`checkJs`); `mapLine` turns its positions back into
|
|
4
|
+
// positions in the HTML.
|
|
5
|
+
import { dirname, relative } from 'node:path';
|
|
6
|
+
import { STORE_FNS, BUILTIN_HELPERS } from '../compiler/expr.js';
|
|
7
|
+
import { schemaToTs, typeName } from './openapi-types.js';
|
|
8
|
+
export function shadow(o) {
|
|
9
|
+
const { unit } = o;
|
|
10
|
+
const out = [];
|
|
11
|
+
const regions = [];
|
|
12
|
+
const line = (s) => out.push(s);
|
|
13
|
+
/** writes `src` from column 1 of the next line, so columns after its first line are the HTML's own */
|
|
14
|
+
const verbatim = (src, loc) => {
|
|
15
|
+
const lines = src.split('\n');
|
|
16
|
+
regions.push({ start: out.length + 1, lines: lines.length, loc });
|
|
17
|
+
out.push(...lines);
|
|
18
|
+
};
|
|
19
|
+
const spec = (target) => {
|
|
20
|
+
let s = relative(dirname(o.file), target).split('\\').join('/');
|
|
21
|
+
if (!s.startsWith('.'))
|
|
22
|
+
s = './' + s;
|
|
23
|
+
return s;
|
|
24
|
+
};
|
|
25
|
+
const moduleSpec = (m) => (m.startsWith('/') ? spec(o.root + m) : m);
|
|
26
|
+
const sinter = spec(o.declarations).replace(/\.d\.ts$/, '');
|
|
27
|
+
const decl = (name) => `import('${sinter}').${name}`;
|
|
28
|
+
line('// @ts-check');
|
|
29
|
+
line(`// Generated by \`sinter check\` from ${relative(o.root, unit.file).split('\\').join('/')}. Do not edit.`);
|
|
30
|
+
if (o.types?.names.length)
|
|
31
|
+
line(`/** @import { ${o.types.names.join(', ')} } from '${spec(o.types.file).replace(/\.(d\.)?ts$/, '')}' */`);
|
|
32
|
+
const refs = unit.exprs.flatMap((e) => [e.resultType, e.returns, e.payload?.type]);
|
|
33
|
+
const specs = new Set([
|
|
34
|
+
...unit.resources.flatMap((r) => (r.openapi && !r.type ? [r.openapi.spec] : [])),
|
|
35
|
+
...refs.flatMap((t) => (t?.kind === 'openapi' ? [t.spec] : [])),
|
|
36
|
+
]);
|
|
37
|
+
for (const i of [...specs].sort())
|
|
38
|
+
line(`/** @import * as $O${i} from '${spec(o.openapi[i]).replace(/\.d\.ts$/, '')}' */`);
|
|
39
|
+
// names a page can see besides its own frames; resources first, as sinter resolves them
|
|
40
|
+
const taken = new Set(unit.resources.map((r) => r.name));
|
|
41
|
+
for (const n of ['$any', '$item', '$el', '$event', '$result', '$error', '$meta', '$url', '$now', '$locale', ...STORE_FNS])
|
|
42
|
+
taken.add(n);
|
|
43
|
+
const builtin = BUILTIN_HELPERS.filter((h) => !taken.has(h));
|
|
44
|
+
for (const h of builtin)
|
|
45
|
+
taken.add(h);
|
|
46
|
+
line(`import { ${builtin.join(', ')} } from '${sinter}';`);
|
|
47
|
+
const userHelpers = o.helpers.names.filter((h) => !taken.has(h));
|
|
48
|
+
if (o.helpers.module && userHelpers.length)
|
|
49
|
+
line(`import { ${userHelpers.join(', ')} } from '${moduleSpec(o.helpers.module)}';`);
|
|
50
|
+
for (const [ns, { module }] of Object.entries(o.helpers.namespaces)) {
|
|
51
|
+
if (!taken.has(ns))
|
|
52
|
+
line(`import * as ${ns} from '${moduleSpec(module)}';`);
|
|
53
|
+
}
|
|
54
|
+
line('const $any = /** @type {any} */ (null);');
|
|
55
|
+
const types = [];
|
|
56
|
+
for (const r of unit.resources) {
|
|
57
|
+
const t = r.type ?? 'any';
|
|
58
|
+
types.push(`${JSON.stringify(r.name)}: ${t}`);
|
|
59
|
+
if (r.type) {
|
|
60
|
+
line('/**');
|
|
61
|
+
line(' * @type {');
|
|
62
|
+
verbatim(r.type, r.loc);
|
|
63
|
+
regions[regions.length - 1].fixed = true;
|
|
64
|
+
line(' * }');
|
|
65
|
+
line(' */');
|
|
66
|
+
}
|
|
67
|
+
else if (r.openapi) {
|
|
68
|
+
const t = schemaToTs(r.openapi.schema, (n) => `$O${r.openapi.spec}.${typeName(n)}`);
|
|
69
|
+
types[types.length - 1] = `${JSON.stringify(r.name)}: ${t}`;
|
|
70
|
+
regions.push({ start: out.length + 1, lines: 1, loc: r.loc, fixed: true });
|
|
71
|
+
line(`/** @type {${t}} ${r.openapi.label.replace(/\*\//g, '')} */`);
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
line('/** @type {any} */');
|
|
75
|
+
regions.push({ start: out.length + 1, lines: 1, loc: r.nameLoc ?? r.loc, fixed: true });
|
|
76
|
+
line(`const ${r.name} = $any;`);
|
|
77
|
+
}
|
|
78
|
+
const names = unit.resources.map((r) => JSON.stringify(r.name)).join(' | ') || 'never';
|
|
79
|
+
line(`/** @type {Record<${names}, ${decl('Meta')}>} */`);
|
|
80
|
+
line('const $meta = $any;');
|
|
81
|
+
line('/** @type {Record<string, any>} */');
|
|
82
|
+
line('const $url = $any;');
|
|
83
|
+
line('/** @type {Date} */');
|
|
84
|
+
line('const $now = $any;');
|
|
85
|
+
line('/** @type {string} */');
|
|
86
|
+
line('let $locale = $any;');
|
|
87
|
+
line(`/** @type {${decl('Store')}<{ ${types.join('; ')} }>} */`);
|
|
88
|
+
line(`const { ${STORE_FNS.join(', ')} } = $any;`);
|
|
89
|
+
line(`/** @template T @param {T} list @returns {${decl('Item')}<T>} */`);
|
|
90
|
+
line('function $item(list) { return $any; }');
|
|
91
|
+
/** a JSDoc tag with a type; a type the app wrote maps its errors to where it was written */
|
|
92
|
+
const tagged = (tag, t, after = '') => {
|
|
93
|
+
if (t.kind === 'ts') {
|
|
94
|
+
line(`/** @${tag} {`);
|
|
95
|
+
verbatim(t.text, t.loc);
|
|
96
|
+
regions[regions.length - 1].fixed = true;
|
|
97
|
+
line(`} */${after}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const text = t.kind === 'openapi'
|
|
101
|
+
? schemaToTs(t.schema, (n) => `$O${t.spec}.${typeName(n)}`)
|
|
102
|
+
: `${t.partial ? 'Partial<' : ''}${decl('Item')}<typeof ${t.resource}>${t.partial ? '>' : ''}`;
|
|
103
|
+
line(`/** @${tag} {${text}} */${after}`);
|
|
104
|
+
};
|
|
105
|
+
/** declarations for the frames of an expression; returns the number of blocks opened */
|
|
106
|
+
const open = (frames) => {
|
|
107
|
+
for (const i of frames) {
|
|
108
|
+
const f = unit.frames[i];
|
|
109
|
+
line('{');
|
|
110
|
+
// declarations map to where the HTML writes the names, for go to definition
|
|
111
|
+
const at = (loc) => { if (loc)
|
|
112
|
+
regions.push({ start: out.length + 1, lines: 1, loc, fixed: true }); };
|
|
113
|
+
if (f.kind === 'state') {
|
|
114
|
+
line('let {');
|
|
115
|
+
f.names.forEach((n, k) => { at(f.locs?.[k]); line(`${n},`); });
|
|
116
|
+
line(`} = ${f.init === null ? '$any' : `$e${f.init}()`};`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
at(f.loc);
|
|
120
|
+
line(`const ${f.item} = ${f.init === null ? '$any' : `$item($e${f.init}())`};`);
|
|
121
|
+
if (f.index) {
|
|
122
|
+
at(f.indexLoc);
|
|
123
|
+
line(`const ${f.index} = 0;`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return frames.length;
|
|
128
|
+
};
|
|
129
|
+
// broken expressions last, so their syntax errors cannot spill into checked code
|
|
130
|
+
const order = unit.exprs.map((e, i) => [e, i]).sort(([a], [b]) => Number(!!a.broken) - Number(!!b.broken));
|
|
131
|
+
let brokenFrom = null;
|
|
132
|
+
order.forEach(([e, i]) => {
|
|
133
|
+
if (e.broken && brokenFrom === null)
|
|
134
|
+
brokenFrom = out.length + 1;
|
|
135
|
+
line('');
|
|
136
|
+
if (e.returns)
|
|
137
|
+
tagged('returns', e.returns);
|
|
138
|
+
line(`function $e${i}() {`);
|
|
139
|
+
const blocks = open(e.frames);
|
|
140
|
+
line(`/** @type {${decl(`El<${JSON.stringify(e.tag)}>`)}} */ const $el = $any;`);
|
|
141
|
+
if (e.payload) {
|
|
142
|
+
if (e.payload.type)
|
|
143
|
+
tagged('type', e.payload.type, ' const $p = $any;');
|
|
144
|
+
else
|
|
145
|
+
line('const $p = $any;');
|
|
146
|
+
// a field the payload type lacks is reported at the start of the expression
|
|
147
|
+
for (const n of e.payload.names) {
|
|
148
|
+
regions.push({ start: out.length + 1, lines: 1, loc: e.loc, fixed: true });
|
|
149
|
+
line(`const ${n} = $p.${n};`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (e.mode === 'value') {
|
|
153
|
+
// a value of the wrong type as a whole is reported on `return`
|
|
154
|
+
regions.push({ start: out.length + 1, lines: 1, loc: e.loc, fixed: true });
|
|
155
|
+
line('return (');
|
|
156
|
+
verbatim(e.src, e.loc);
|
|
157
|
+
line(');');
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
line(`/** @type {${decl(`Ev<${JSON.stringify(e.event ?? '')}>`)}} */ const $event = $any;`);
|
|
161
|
+
if (e.result) {
|
|
162
|
+
if (e.resultType)
|
|
163
|
+
tagged('type', e.resultType, ' const $result = $any;');
|
|
164
|
+
else
|
|
165
|
+
line('/** @type {any} */ const $result = $any;');
|
|
166
|
+
}
|
|
167
|
+
if (e.error)
|
|
168
|
+
line(`/** @type {${decl('Err')}} */ const $error = $any;`);
|
|
169
|
+
verbatim(e.src, e.loc);
|
|
170
|
+
line(';');
|
|
171
|
+
}
|
|
172
|
+
for (let b = 0; b < blocks; b++)
|
|
173
|
+
line('}');
|
|
174
|
+
line('}');
|
|
175
|
+
});
|
|
176
|
+
return { text: out.join('\n') + '\n', regions, brokenFrom };
|
|
177
|
+
}
|
|
178
|
+
/** Maps a 1-based shadow position to the HTML, or null for generated lines. */
|
|
179
|
+
export function mapPosition(regions, line, col) {
|
|
180
|
+
for (const r of regions) {
|
|
181
|
+
if (line < r.start || line >= r.start + r.lines)
|
|
182
|
+
continue;
|
|
183
|
+
if (r.fixed)
|
|
184
|
+
return r.loc;
|
|
185
|
+
const k = line - r.start;
|
|
186
|
+
return { ...r.loc, line: r.loc.line + k, col: k === 0 ? r.loc.col + col - 1 : col };
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
/** `sinter.d.ts`: the types the shadow files refer to. */
|
|
191
|
+
export const DECLARATIONS = `// Generated by \`sinter check\`. Do not edit.
|
|
192
|
+
export interface Meta {
|
|
193
|
+
loading: boolean;
|
|
194
|
+
error: Err | null;
|
|
195
|
+
loadedAt: number | null;
|
|
196
|
+
pending: number;
|
|
197
|
+
total: number | null;
|
|
198
|
+
page?: number;
|
|
199
|
+
pages?: number | null;
|
|
200
|
+
from?: number | null;
|
|
201
|
+
to?: number | null;
|
|
202
|
+
hasPrev?: boolean;
|
|
203
|
+
hasNext?: boolean;
|
|
204
|
+
}
|
|
205
|
+
export interface Err { status: number; body: any; message: string }
|
|
206
|
+
export type Item<T> = NonNullable<T> extends readonly (infer U)[] ? U : NonNullable<T> extends Iterable<infer U> ? U : any;
|
|
207
|
+
export type El<K extends string> = K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : HTMLElement;
|
|
208
|
+
export type Ev<K extends string> = K extends keyof HTMLElementEventMap ? HTMLElementEventMap[K] : CustomEvent;
|
|
209
|
+
export interface Store<R> {
|
|
210
|
+
refresh(name: keyof R & string): void;
|
|
211
|
+
upsert<K extends keyof R & string>(name: K, item: Item<R[K]>): void;
|
|
212
|
+
patch<K extends keyof R & string>(name: K, item: Partial<Item<R[K]>>, set: Partial<Item<R[K]>>): void;
|
|
213
|
+
remove<K extends keyof R & string>(name: K, item: Partial<Item<R[K]>>): void;
|
|
214
|
+
page(name: keyof R & string, to: 'next' | 'prev' | 'first' | 'last' | number): void;
|
|
215
|
+
close(): void;
|
|
216
|
+
reset(): void;
|
|
217
|
+
emit(event: string, detail?: unknown): void;
|
|
218
|
+
}
|
|
219
|
+
type Style = 'short' | 'medium' | 'long' | 'full';
|
|
220
|
+
export function date(v: unknown, style?: Style): string;
|
|
221
|
+
export function time(v: unknown, style?: Style): string;
|
|
222
|
+
export function datetime(v: unknown, style?: Style): string;
|
|
223
|
+
export function relative(v: unknown, now?: unknown): string;
|
|
224
|
+
export function number(v: unknown, opts?: Intl.NumberFormatOptions): string;
|
|
225
|
+
export function bytes(v: unknown): string;
|
|
226
|
+
`;
|
|
227
|
+
/**
|
|
228
|
+
* Maps a 1-based position in an HTML file to the shadow, or null when it is not inside an
|
|
229
|
+
* expression. `lines` are the shadow's lines.
|
|
230
|
+
*/
|
|
231
|
+
export function shadowPosition(regions, lines, file, line, col) {
|
|
232
|
+
for (const r of regions) {
|
|
233
|
+
if (r.fixed || r.loc.file !== file)
|
|
234
|
+
continue;
|
|
235
|
+
const k = line - r.loc.line;
|
|
236
|
+
if (k < 0 || k >= r.lines)
|
|
237
|
+
continue;
|
|
238
|
+
const text = lines[r.start - 1 + k] ?? '';
|
|
239
|
+
const c = k === 0 ? col - r.loc.col + 1 : col;
|
|
240
|
+
if (c < 1 || c > text.length + 1)
|
|
241
|
+
continue;
|
|
242
|
+
return { line: r.start + k, col: c };
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `sinter check [--root <dir>] [page.html …]`: compiles the pages and type-checks
|
|
3
|
+
// their expressions. Without pages, it reads them and the plugin options from the
|
|
4
|
+
// Vite config. `sinter lsp`: the language server for editors.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { relative, resolve } from 'node:path';
|
|
7
|
+
import { check } from './check/index.js';
|
|
8
|
+
import { compile, formatDiagnostic } from './compiler/index.js';
|
|
9
|
+
import { project, viteProject } from './project.js';
|
|
10
|
+
const USAGE = 'usage: sinter check [--root <dir>] [page.html …]\n sinter lsp [--stdio]';
|
|
11
|
+
async function main(argv) {
|
|
12
|
+
const [command, ...rest] = argv;
|
|
13
|
+
if (command === 'lsp')
|
|
14
|
+
return lsp();
|
|
15
|
+
if (command !== 'check') {
|
|
16
|
+
console.error(USAGE);
|
|
17
|
+
return 2;
|
|
18
|
+
}
|
|
19
|
+
let root = process.cwd();
|
|
20
|
+
const pages = [];
|
|
21
|
+
for (let i = 0; i < rest.length; i++) {
|
|
22
|
+
if (rest[i] === '--root')
|
|
23
|
+
root = resolve(rest[++i] ?? '.');
|
|
24
|
+
else
|
|
25
|
+
pages.push(rest[i]);
|
|
26
|
+
}
|
|
27
|
+
let options = {};
|
|
28
|
+
let files = pages.map((p) => resolve(root, p));
|
|
29
|
+
if (!files.length)
|
|
30
|
+
({ pages: files, options } = await viteProject(root));
|
|
31
|
+
if (!files.length) {
|
|
32
|
+
console.error(`sinter check: no pages found; pass them as arguments\n${USAGE}`);
|
|
33
|
+
return 2;
|
|
34
|
+
}
|
|
35
|
+
const p = project(root, options);
|
|
36
|
+
const diagnostics = [...p.diagnostics];
|
|
37
|
+
const units = [];
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const out = compile(readFileSync(file, 'utf8'), { file, root, dev: false, helpers: p.helpers, config: p.config, readFile: p.readFile, check: true, openapi: p.openapi });
|
|
40
|
+
diagnostics.push(...out.diagnostics);
|
|
41
|
+
if (out.check)
|
|
42
|
+
units.push(out.check);
|
|
43
|
+
}
|
|
44
|
+
const r = check({ root, units, helpers: p.helpers, types: p.types, openapi: p.openapi });
|
|
45
|
+
diagnostics.push(...r.diagnostics);
|
|
46
|
+
for (const d of diagnostics)
|
|
47
|
+
console.log(formatDiagnostic({ ...d, file: relative(process.cwd(), d.file) || d.file }) + '\n');
|
|
48
|
+
const errors = diagnostics.filter((d) => d.severity === 'error').length;
|
|
49
|
+
const warnings = diagnostics.length - errors;
|
|
50
|
+
console.log(`sinter check: ${files.length} page(s), ${errors} error(s), ${warnings} warning(s); ${r.typed} of ${r.resources} resources typed${r.openapi ? ` (${r.openapi} via OpenAPI)` : ''}`);
|
|
51
|
+
return errors ? 1 : 0;
|
|
52
|
+
}
|
|
53
|
+
/** Language server over stdio; stdout carries only protocol messages. */
|
|
54
|
+
async function lsp() {
|
|
55
|
+
const toStderr = (...args) => process.stderr.write(args.map(String).join(' ') + '\n');
|
|
56
|
+
console.log = console.info = console.warn = console.debug = toStderr;
|
|
57
|
+
const { startServer } = await import('./lsp/server.js');
|
|
58
|
+
return new Promise((done) => {
|
|
59
|
+
const conn = startServer(process.stdin, process.stdout, { exit: (code) => process.exit(code) });
|
|
60
|
+
const closed = conn.onClose;
|
|
61
|
+
conn.onClose = () => { closed?.(); done(0); };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (err) => {
|
|
65
|
+
console.error(`sinter check: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
process.exitCode = 2;
|
|
67
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Where an expression (or a `type` attribute value) starts in its source file. */
|
|
2
|
+
export interface CheckLoc {
|
|
3
|
+
file: string;
|
|
4
|
+
line: number;
|
|
5
|
+
col: number;
|
|
6
|
+
/** include chain, innermost first (`page.html:12`), for messages */
|
|
7
|
+
chain: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A scope frame: a `data-sn-state` object or a `data-sn-for` loop. `init` is the
|
|
11
|
+
* expression that creates it (the state literal, the list), or null when that
|
|
12
|
+
* expression did not compile.
|
|
13
|
+
*/
|
|
14
|
+
export type CheckFrame =
|
|
15
|
+
/** `locs[i]`: where `names[i]` is written in the literal */
|
|
16
|
+
{
|
|
17
|
+
kind: 'state';
|
|
18
|
+
names: string[];
|
|
19
|
+
init: number | null;
|
|
20
|
+
locs?: CheckLoc[];
|
|
21
|
+
}
|
|
22
|
+
/** `loc`, `indexLoc`: where the item and index names are written */
|
|
23
|
+
| {
|
|
24
|
+
kind: 'loop';
|
|
25
|
+
item: string;
|
|
26
|
+
index: string | null;
|
|
27
|
+
init: number | null;
|
|
28
|
+
loc?: CheckLoc;
|
|
29
|
+
indexLoc?: CheckLoc;
|
|
30
|
+
};
|
|
31
|
+
/** A type the checker writes for something other than a resource. */
|
|
32
|
+
export type TypeRef =
|
|
33
|
+
/** written by the app (`type` on `<sn-event>`); errors in it point at `loc` */
|
|
34
|
+
{
|
|
35
|
+
kind: 'ts';
|
|
36
|
+
text: string;
|
|
37
|
+
loc: CheckLoc;
|
|
38
|
+
}
|
|
39
|
+
/** a schema of OpenAPI spec number `spec` */
|
|
40
|
+
| {
|
|
41
|
+
kind: 'openapi';
|
|
42
|
+
spec: number;
|
|
43
|
+
schema: unknown;
|
|
44
|
+
}
|
|
45
|
+
/** an item of a resource on the page, or a partial one */
|
|
46
|
+
| {
|
|
47
|
+
kind: 'item';
|
|
48
|
+
resource: string;
|
|
49
|
+
partial?: boolean;
|
|
50
|
+
};
|
|
51
|
+
export interface CheckExpr {
|
|
52
|
+
src: string;
|
|
53
|
+
/** `value` returns the expression; `handler` runs it as statements */
|
|
54
|
+
mode: 'value' | 'handler';
|
|
55
|
+
loc: CheckLoc;
|
|
56
|
+
/** frames visible here, in the order sinter resolves them last: states outer to inner, then loops outer to inner */
|
|
57
|
+
frames: number[];
|
|
58
|
+
/** tag of the element the expression sits on, for `$el` */
|
|
59
|
+
tag: string;
|
|
60
|
+
/** DOM event that runs a handler, for `$event` */
|
|
61
|
+
event: string | null;
|
|
62
|
+
/** `$result` (then) or `$error` (catch) is in scope */
|
|
63
|
+
result: boolean;
|
|
64
|
+
error: boolean;
|
|
65
|
+
/** type of `$result`, when the action matched an OpenAPI operation */
|
|
66
|
+
resultType?: TypeRef;
|
|
67
|
+
/** the value must be assignable to this (`data-sn-body`, `<sn-event>` `item` and `set`) */
|
|
68
|
+
returns?: TypeRef;
|
|
69
|
+
/** payload expressions (`<sn-event>`, `cursor`): the payload fields read, and the payload's type (null: any) */
|
|
70
|
+
payload?: {
|
|
71
|
+
names: string[];
|
|
72
|
+
type: TypeRef | null;
|
|
73
|
+
};
|
|
74
|
+
/** the compiler reported an error in it (it may not even parse); kept for editor completion, never type-checked */
|
|
75
|
+
broken?: boolean;
|
|
76
|
+
}
|
|
77
|
+
export interface CheckResource {
|
|
78
|
+
name: string;
|
|
79
|
+
/** value of `type`, a TypeScript type; null when absent */
|
|
80
|
+
type: string | null;
|
|
81
|
+
/** start of the `type` value, or of the element when absent */
|
|
82
|
+
loc: CheckLoc;
|
|
83
|
+
/** start of the `name` value, where go to definition lands */
|
|
84
|
+
nameLoc?: CheckLoc;
|
|
85
|
+
/** without `type`: the OpenAPI operation `src` matched and the schema of what `items` selects */
|
|
86
|
+
openapi?: {
|
|
87
|
+
spec: number;
|
|
88
|
+
label: string;
|
|
89
|
+
schema: unknown;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export interface CheckUnit {
|
|
93
|
+
file: string;
|
|
94
|
+
resources: CheckResource[];
|
|
95
|
+
frames: CheckFrame[];
|
|
96
|
+
exprs: CheckExpr[];
|
|
97
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Diagnostic } from './diagnostics.js';
|
|
2
|
+
import type { CheckUnit } from './check-unit.js';
|
|
3
|
+
import { type OpenApiSpec } from './openapi.js';
|
|
4
|
+
import type { IR, NodeDef } from '../ir.js';
|
|
5
|
+
export interface HelperManifest {
|
|
6
|
+
/** import specifier of the helpers module, or null */
|
|
7
|
+
module: string | null;
|
|
8
|
+
names: string[];
|
|
9
|
+
namespaces: Record<string, {
|
|
10
|
+
module: string;
|
|
11
|
+
members: string[];
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export interface CompileOptions {
|
|
15
|
+
file: string;
|
|
16
|
+
root: string;
|
|
17
|
+
readFile: (path: string) => string;
|
|
18
|
+
helpers: HelperManifest;
|
|
19
|
+
dev: boolean;
|
|
20
|
+
/** URL the injected `<script type="module">` loads the page module from */
|
|
21
|
+
moduleSrc?: string;
|
|
22
|
+
/** app configuration module imported before mount (e.g. `/src/sinter.config.js`) */
|
|
23
|
+
config?: string | null;
|
|
24
|
+
/** also return a check unit for `sinter check`, and warn about resources without `type` */
|
|
25
|
+
check?: boolean;
|
|
26
|
+
/** parsed OpenAPI specs: resources without `type` are matched against them */
|
|
27
|
+
openapi?: OpenApiSpec[];
|
|
28
|
+
}
|
|
29
|
+
export interface CompileResult {
|
|
30
|
+
html: string;
|
|
31
|
+
module: string;
|
|
32
|
+
diagnostics: Diagnostic[];
|
|
33
|
+
files: string[];
|
|
34
|
+
/** with `check: true` */
|
|
35
|
+
check?: CheckUnit;
|
|
36
|
+
}
|
|
37
|
+
export interface CompiledParts {
|
|
38
|
+
html: string;
|
|
39
|
+
ir: IR;
|
|
40
|
+
fns: string[];
|
|
41
|
+
features: string[];
|
|
42
|
+
/** helper names imported from the helpers module or the core */
|
|
43
|
+
helpers: {
|
|
44
|
+
user: string[];
|
|
45
|
+
builtin: string[];
|
|
46
|
+
};
|
|
47
|
+
namespaces: Record<string, string[]>;
|
|
48
|
+
diagnostics: Diagnostic[];
|
|
49
|
+
files: string[];
|
|
50
|
+
/** with `check: true` */
|
|
51
|
+
check?: CheckUnit;
|
|
52
|
+
}
|
|
53
|
+
export declare function compileParts(html: string, options: CompileOptions): CompiledParts;
|
|
54
|
+
export declare function normalize(p: string): string;
|
|
55
|
+
export declare function dirname(p: string): string;
|
|
56
|
+
export declare function basename(p: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* A row template's element lookup as straight-line code: each path prefix is walked once
|
|
59
|
+
* (`firstChild` / `nextSibling`), and an element whose tag does not match comes out null.
|
|
60
|
+
* The argument is the clone's first content node (the row element itself for a one-element row).
|
|
61
|
+
*/
|
|
62
|
+
export declare function findCode(nodes: NodeDef[]): string;
|