@tiaforge-ui/sinter 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +158 -2
- package/dist/cache.d.ts +5 -0
- package/dist/cache.js +95 -0
- package/dist/check/index.d.ts +85 -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 +49 -0
- package/dist/check/shadow.js +279 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +67 -0
- package/dist/compiler/check-unit.d.ts +109 -0
- package/dist/compiler/check-unit.js +3 -0
- package/dist/compiler/compile.d.ts +66 -0
- package/dist/compiler/compile.js +1316 -0
- package/dist/compiler/diagnostic-docs.d.ts +10 -0
- package/dist/compiler/diagnostic-docs.js +142 -0
- package/dist/compiler/diagnostics.d.ts +57 -0
- package/dist/compiler/diagnostics.js +77 -0
- package/dist/compiler/expr.d.ts +97 -0
- package/dist/compiler/expr.js +853 -0
- package/dist/compiler/index.d.ts +22 -0
- package/dist/compiler/index.js +160 -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 +39 -0
- package/dist/dev.d.ts +10 -0
- package/dist/dev.js +92 -0
- package/dist/each.d.ts +3 -0
- package/dist/each.js +121 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/ir.d.ts +152 -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 +228 -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 +198 -0
- package/dist/persist.d.ts +3 -0
- package/dist/persist.js +55 -0
- package/dist/project.d.ts +42 -0
- package/dist/project.js +103 -0
- package/dist/runtime/app.d.ts +152 -0
- package/dist/runtime/app.js +1256 -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 +98 -0
- package/dist/runtime/store.js +255 -0
- package/dist/sse.d.ts +7 -0
- package/dist/sse.js +209 -0
- package/dist/testing/index.d.ts +73 -0
- package/dist/testing/index.js +359 -0
- package/dist/vite.d.ts +11 -0
- package/dist/vite.js +96 -0
- package/dist/ws.d.ts +18 -0
- package/dist/ws.js +57 -0
- package/docs/diagnostics.md +283 -0
- package/html-custom-data.json +250 -0
- package/llms.txt +632 -0
- package/package.json +93 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CheckLoc, CheckUnit } from '../compiler/check-unit.js';
|
|
2
|
+
import type { HelperManifest } from '../compiler/compile.js';
|
|
3
|
+
export interface ShadowOptions {
|
|
4
|
+
unit: CheckUnit;
|
|
5
|
+
/** absolute path the shadow file is written to */
|
|
6
|
+
file: string;
|
|
7
|
+
/** project root; root-relative module specifiers (`/src/x.js`) resolve against it */
|
|
8
|
+
root: string;
|
|
9
|
+
/** absolute path of the declarations sinter writes next to the shadow files (`sinter.d.ts`) */
|
|
10
|
+
declarations: string;
|
|
11
|
+
/** the types module and the names it exports */
|
|
12
|
+
types: {
|
|
13
|
+
file: string;
|
|
14
|
+
names: string[];
|
|
15
|
+
} | null;
|
|
16
|
+
helpers: HelperManifest;
|
|
17
|
+
/** the actions module, for `data-sn-call` arguments and results */
|
|
18
|
+
actions?: HelperManifest;
|
|
19
|
+
/** absolute paths of the declarations generated per OpenAPI spec, by spec index */
|
|
20
|
+
openapi?: string[];
|
|
21
|
+
}
|
|
22
|
+
/** A run of shadow lines that came from the HTML. */
|
|
23
|
+
export interface Region {
|
|
24
|
+
/** first shadow line, 1-based */
|
|
25
|
+
start: number;
|
|
26
|
+
lines: number;
|
|
27
|
+
loc: CheckLoc;
|
|
28
|
+
/** every line maps to `loc` itself (a `type` attribute in a JSDoc comment) */
|
|
29
|
+
fixed?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface Shadow {
|
|
32
|
+
text: string;
|
|
33
|
+
regions: Region[];
|
|
34
|
+
/** first line of the broken expressions (written last); nothing from there on is reported */
|
|
35
|
+
brokenFrom: number | null;
|
|
36
|
+
}
|
|
37
|
+
export declare function shadow(o: ShadowOptions): Shadow;
|
|
38
|
+
/** Maps a 1-based shadow position to the HTML, or null for generated lines. */
|
|
39
|
+
export declare function mapPosition(regions: Region[], line: number, col: number): CheckLoc | null;
|
|
40
|
+
/** `sinter.d.ts`: the types the shadow files refer to. */
|
|
41
|
+
export declare const DECLARATIONS = "// Generated by `sinter check`. Do not edit.\nexport interface Meta {\n loading: boolean;\n error: Err | null;\n loadedAt: number | null;\n pending: number;\n total: number | null;\n page?: number | null;\n pages?: number | null;\n from?: number | null;\n to?: number | null;\n hasPrev?: boolean;\n hasNext?: boolean;\n}\nexport interface Err { status: number | null; body: any; message: string }\nexport interface CallContext { signal: AbortSignal; request(url: string, init?: { method?: string; body?: unknown; headers?: Record<string, string>; signal?: AbortSignal }): Promise<any>; el: Element | null }\n/** an action's parameters without its trailing context */\nexport type CallArgs<F> = F extends (...a: infer P) => any ? (P extends [...infer A, infer L] ? ([L] extends [CallContext] ? A : P) : P) : never;\nexport type Item<T> = NonNullable<T> extends readonly (infer U)[] ? U : NonNullable<T> extends Iterable<infer U> ? U : any;\nexport type El<K extends string> = K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : HTMLElement;\nexport type Ev<K extends string> = K extends keyof HTMLElementEventMap ? HTMLElementEventMap[K] : CustomEvent;\nexport interface Store<R> {\n refresh(name: keyof R & string): void;\n upsert<K extends keyof R & string>(name: K, item: Item<R[K]>): void;\n patch<K extends keyof R & string>(name: K, item: Partial<Item<R[K]>>, set: Partial<Item<R[K]>>): void;\n remove<K extends keyof R & string>(name: K, item: Partial<Item<R[K]>>): void;\n page(name: keyof R & string, to: 'next' | 'prev' | 'first' | 'last' | number): void;\n close(): void;\n reset(): void;\n emit(event: string, detail?: unknown): void;\n}\ntype Style = 'short' | 'medium' | 'long' | 'full';\nexport function date(v: unknown, style?: Style): string;\nexport function time(v: unknown, style?: Style): string;\nexport function datetime(v: unknown, style?: Style): string;\nexport function relative(v: unknown, now?: unknown): string;\nexport function number(v: unknown, opts?: Intl.NumberFormatOptions): string;\nexport function bytes(v: unknown): string;\n";
|
|
42
|
+
/**
|
|
43
|
+
* Maps a 1-based position in an HTML file to the shadow, or null when it is not inside an
|
|
44
|
+
* expression. `lines` are the shadow's lines.
|
|
45
|
+
*/
|
|
46
|
+
export declare function shadowPosition(regions: Region[], lines: string[], file: string, line: number, col: number): {
|
|
47
|
+
line: number;
|
|
48
|
+
col: number;
|
|
49
|
+
} | null;
|
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
if (o.actions?.module && (unit.resources.some((r) => r.call) || unit.exprs.some((e) => e.returns?.kind === 'call' || e.resultType?.kind === 'call'))) {
|
|
55
|
+
line(`import * as $A from '${moduleSpec(o.actions.module)}';`);
|
|
56
|
+
}
|
|
57
|
+
line('const $any = /** @type {any} */ (null);');
|
|
58
|
+
/** what a resource fed by `call` holds: the awaited result, at its `items` path */
|
|
59
|
+
const callResult = (c) => `Awaited<ReturnType<typeof $A.${c.name}>>${(c.items ? c.items.split('.') : []).map((k) => `[${JSON.stringify(k)}]`).join('')}`;
|
|
60
|
+
const types = [];
|
|
61
|
+
for (const r of unit.resources) {
|
|
62
|
+
const t = r.type ?? 'any';
|
|
63
|
+
types.push(`${JSON.stringify(r.name)}: ${t}`);
|
|
64
|
+
if (r.type) {
|
|
65
|
+
line('/**');
|
|
66
|
+
line(' * @type {');
|
|
67
|
+
verbatim(r.type, r.loc);
|
|
68
|
+
regions[regions.length - 1].fixed = true;
|
|
69
|
+
line(' * }');
|
|
70
|
+
line(' */');
|
|
71
|
+
}
|
|
72
|
+
else if (r.call) {
|
|
73
|
+
const t = callResult(r.call);
|
|
74
|
+
types[types.length - 1] = `${JSON.stringify(r.name)}: ${t}`;
|
|
75
|
+
line(`/** @type {${t}} */`);
|
|
76
|
+
}
|
|
77
|
+
else if (r.openapi) {
|
|
78
|
+
const t = schemaToTs(r.openapi.schema, (n) => `$O${r.openapi.spec}.${typeName(n)}`);
|
|
79
|
+
types[types.length - 1] = `${JSON.stringify(r.name)}: ${t}`;
|
|
80
|
+
regions.push({ start: out.length + 1, lines: 1, loc: r.loc, fixed: true });
|
|
81
|
+
line(`/** @type {${t}} ${r.openapi.label.replace(/\*\//g, '')} */`);
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
line('/** @type {any} */');
|
|
85
|
+
regions.push({ start: out.length + 1, lines: 1, loc: r.nameLoc ?? r.loc, fixed: true });
|
|
86
|
+
line(`const ${r.name} = $any;`);
|
|
87
|
+
// with `type`, what the action returns (at `items`) must fit it; reported on `type`
|
|
88
|
+
if (r.call && r.type) {
|
|
89
|
+
// own line: TypeScript drops a JSDoc tag that follows `{` on the same line
|
|
90
|
+
line('{');
|
|
91
|
+
regions.push({ start: out.length + 1, lines: 1, loc: r.loc, fixed: true });
|
|
92
|
+
line(`/** @type {${r.type.replace(/\s+/g, ' ')}} */ const $fits = /** @type {${callResult(r.call)}} */ ($any); void $fits;`);
|
|
93
|
+
line('}');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const names = unit.resources.map((r) => JSON.stringify(r.name)).join(' | ') || 'never';
|
|
97
|
+
line(`/** @type {Record<${names}, ${decl('Meta')}>} */`);
|
|
98
|
+
line('const $meta = $any;');
|
|
99
|
+
line('/** @type {Record<string, any>} */');
|
|
100
|
+
line('const $url = $any;');
|
|
101
|
+
line('/** @type {Date} */');
|
|
102
|
+
line('const $now = $any;');
|
|
103
|
+
line('/** @type {string} */');
|
|
104
|
+
line('let $locale = $any;');
|
|
105
|
+
line(`/** @type {${decl('Store')}<{ ${types.join('; ')} }>} */`);
|
|
106
|
+
line(`const { ${STORE_FNS.join(', ')} } = $any;`);
|
|
107
|
+
line(`/** @template T @param {T} list @returns {${decl('Item')}<T>} */`);
|
|
108
|
+
line('function $item(list) { return $any; }');
|
|
109
|
+
/** a JSDoc tag with a type; a type the app wrote maps its errors to where it was written */
|
|
110
|
+
const tagged = (tag, t, after = '') => {
|
|
111
|
+
if (t.kind === 'ts') {
|
|
112
|
+
line(`/** @${tag} {`);
|
|
113
|
+
verbatim(t.text, t.loc);
|
|
114
|
+
regions[regions.length - 1].fixed = true;
|
|
115
|
+
line(`} */${after}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const text = t.kind === 'openapi'
|
|
119
|
+
? schemaToTs(t.schema, (n) => `$O${t.spec}.${typeName(n)}`)
|
|
120
|
+
: t.kind === 'call'
|
|
121
|
+
? (t.args ? `${decl('CallArgs')}<typeof $A.${t.name}>` : `Awaited<ReturnType<typeof $A.${t.name}>>`)
|
|
122
|
+
: `${t.partial ? 'Partial<' : ''}${decl('Item')}<typeof ${t.resource}>${t.partial ? '>' : ''}`;
|
|
123
|
+
line(`/** @${tag} {${text}} */${after}`);
|
|
124
|
+
};
|
|
125
|
+
/** declarations for the frames of an expression; returns the number of blocks opened */
|
|
126
|
+
const open = (frames) => {
|
|
127
|
+
for (const i of frames) {
|
|
128
|
+
const f = unit.frames[i];
|
|
129
|
+
line('{');
|
|
130
|
+
// declarations map to where the HTML writes the names, for go to definition
|
|
131
|
+
const at = (loc) => { if (loc)
|
|
132
|
+
regions.push({ start: out.length + 1, lines: 1, loc, fixed: true }); };
|
|
133
|
+
if (f.kind === 'state') {
|
|
134
|
+
line('let {');
|
|
135
|
+
f.names.forEach((n, k) => { at(f.locs?.[k]); line(`${n},`); });
|
|
136
|
+
line(`} = ${f.init === null ? '$any' : `$e${f.init}()`};`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
at(f.loc);
|
|
140
|
+
line(`const ${f.item} = ${f.init === null ? '$any' : `$item($e${f.init}())`};`);
|
|
141
|
+
if (f.index) {
|
|
142
|
+
at(f.indexLoc);
|
|
143
|
+
line(`const ${f.index} = 0;`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return frames.length;
|
|
148
|
+
};
|
|
149
|
+
// broken expressions last, so their syntax errors cannot spill into checked code
|
|
150
|
+
const order = unit.exprs.map((e, i) => [e, i]).sort(([a], [b]) => Number(!!a.broken) - Number(!!b.broken));
|
|
151
|
+
let brokenFrom = null;
|
|
152
|
+
order.forEach(([e, i]) => {
|
|
153
|
+
if (e.broken && brokenFrom === null)
|
|
154
|
+
brokenFrom = out.length + 1;
|
|
155
|
+
line('');
|
|
156
|
+
if (e.returns)
|
|
157
|
+
tagged('returns', e.returns);
|
|
158
|
+
line(`function $e${i}() {`);
|
|
159
|
+
const blocks = open(e.frames);
|
|
160
|
+
line(`/** @type {${decl(`El<${JSON.stringify(e.tag)}>`)}} */ const $el = $any;`);
|
|
161
|
+
if (e.payload) {
|
|
162
|
+
if (e.payload.type)
|
|
163
|
+
tagged('type', e.payload.type, ' const $p = $any;');
|
|
164
|
+
else
|
|
165
|
+
line('const $p = $any;');
|
|
166
|
+
// a field the payload type lacks is reported at the start of the expression
|
|
167
|
+
for (const n of e.payload.names) {
|
|
168
|
+
regions.push({ start: out.length + 1, lines: 1, loc: e.loc, fixed: true });
|
|
169
|
+
line(`const ${n} = $p.${n};`);
|
|
170
|
+
}
|
|
171
|
+
// a state name shadows a payload field of the same name: an error when the payload type has it
|
|
172
|
+
if (e.payload.type) {
|
|
173
|
+
for (const n of e.payload.states ?? []) {
|
|
174
|
+
const msg = `state \`${n}\` shadows the payload field \`${n}\`: rename the state`;
|
|
175
|
+
regions.push({ start: out.length + 1, lines: 1, loc: e.loc, fixed: true });
|
|
176
|
+
line(`/** @type {${JSON.stringify(n)} extends keyof typeof $p ? ${JSON.stringify(msg)} : true} */ const $shadows_${n} = true; void $shadows_${n};`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (e.mode === 'value') {
|
|
181
|
+
// action arguments and bodies are taken when the trigger fires
|
|
182
|
+
if (e.event !== null)
|
|
183
|
+
line(`/** @type {${decl(`Ev<${JSON.stringify(e.event)}>`)}} */ const $event = $any;`);
|
|
184
|
+
// a value of the wrong type as a whole is reported on `return`
|
|
185
|
+
regions.push({ start: out.length + 1, lines: 1, loc: e.loc, fixed: true });
|
|
186
|
+
line('return (');
|
|
187
|
+
verbatim(e.src, e.loc);
|
|
188
|
+
line(');');
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
line(`/** @type {${decl(`Ev<${JSON.stringify(e.event ?? '')}>`)}} */ const $event = $any;`);
|
|
192
|
+
if (e.result) {
|
|
193
|
+
if (e.resultType)
|
|
194
|
+
tagged('type', e.resultType, ' const $result = $any;');
|
|
195
|
+
else
|
|
196
|
+
line('/** @type {any} */ const $result = $any;');
|
|
197
|
+
}
|
|
198
|
+
if (e.error)
|
|
199
|
+
line(`/** @type {${decl('Err')}} */ const $error = $any;`);
|
|
200
|
+
verbatim(e.src, e.loc);
|
|
201
|
+
line(';');
|
|
202
|
+
}
|
|
203
|
+
for (let b = 0; b < blocks; b++)
|
|
204
|
+
line('}');
|
|
205
|
+
line('}');
|
|
206
|
+
});
|
|
207
|
+
return { text: out.join('\n') + '\n', regions, brokenFrom };
|
|
208
|
+
}
|
|
209
|
+
/** Maps a 1-based shadow position to the HTML, or null for generated lines. */
|
|
210
|
+
export function mapPosition(regions, line, col) {
|
|
211
|
+
for (const r of regions) {
|
|
212
|
+
if (line < r.start || line >= r.start + r.lines)
|
|
213
|
+
continue;
|
|
214
|
+
if (r.fixed)
|
|
215
|
+
return r.loc;
|
|
216
|
+
const k = line - r.start;
|
|
217
|
+
return { ...r.loc, line: r.loc.line + k, col: k === 0 ? r.loc.col + col - 1 : col };
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
/** `sinter.d.ts`: the types the shadow files refer to. */
|
|
222
|
+
export const DECLARATIONS = `// Generated by \`sinter check\`. Do not edit.
|
|
223
|
+
export interface Meta {
|
|
224
|
+
loading: boolean;
|
|
225
|
+
error: Err | null;
|
|
226
|
+
loadedAt: number | null;
|
|
227
|
+
pending: number;
|
|
228
|
+
total: number | null;
|
|
229
|
+
page?: number | null;
|
|
230
|
+
pages?: number | null;
|
|
231
|
+
from?: number | null;
|
|
232
|
+
to?: number | null;
|
|
233
|
+
hasPrev?: boolean;
|
|
234
|
+
hasNext?: boolean;
|
|
235
|
+
}
|
|
236
|
+
export interface Err { status: number | null; body: any; message: string }
|
|
237
|
+
export interface CallContext { signal: AbortSignal; request(url: string, init?: { method?: string; body?: unknown; headers?: Record<string, string>; signal?: AbortSignal }): Promise<any>; el: Element | null }
|
|
238
|
+
/** an action's parameters without its trailing context */
|
|
239
|
+
export type CallArgs<F> = F extends (...a: infer P) => any ? (P extends [...infer A, infer L] ? ([L] extends [CallContext] ? A : P) : P) : never;
|
|
240
|
+
export type Item<T> = NonNullable<T> extends readonly (infer U)[] ? U : NonNullable<T> extends Iterable<infer U> ? U : any;
|
|
241
|
+
export type El<K extends string> = K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : HTMLElement;
|
|
242
|
+
export type Ev<K extends string> = K extends keyof HTMLElementEventMap ? HTMLElementEventMap[K] : CustomEvent;
|
|
243
|
+
export interface Store<R> {
|
|
244
|
+
refresh(name: keyof R & string): void;
|
|
245
|
+
upsert<K extends keyof R & string>(name: K, item: Item<R[K]>): void;
|
|
246
|
+
patch<K extends keyof R & string>(name: K, item: Partial<Item<R[K]>>, set: Partial<Item<R[K]>>): void;
|
|
247
|
+
remove<K extends keyof R & string>(name: K, item: Partial<Item<R[K]>>): void;
|
|
248
|
+
page(name: keyof R & string, to: 'next' | 'prev' | 'first' | 'last' | number): void;
|
|
249
|
+
close(): void;
|
|
250
|
+
reset(): void;
|
|
251
|
+
emit(event: string, detail?: unknown): void;
|
|
252
|
+
}
|
|
253
|
+
type Style = 'short' | 'medium' | 'long' | 'full';
|
|
254
|
+
export function date(v: unknown, style?: Style): string;
|
|
255
|
+
export function time(v: unknown, style?: Style): string;
|
|
256
|
+
export function datetime(v: unknown, style?: Style): string;
|
|
257
|
+
export function relative(v: unknown, now?: unknown): string;
|
|
258
|
+
export function number(v: unknown, opts?: Intl.NumberFormatOptions): string;
|
|
259
|
+
export function bytes(v: unknown): string;
|
|
260
|
+
`;
|
|
261
|
+
/**
|
|
262
|
+
* Maps a 1-based position in an HTML file to the shadow, or null when it is not inside an
|
|
263
|
+
* expression. `lines` are the shadow's lines.
|
|
264
|
+
*/
|
|
265
|
+
export function shadowPosition(regions, lines, file, line, col) {
|
|
266
|
+
for (const r of regions) {
|
|
267
|
+
if (r.fixed || r.loc.file !== file)
|
|
268
|
+
continue;
|
|
269
|
+
const k = line - r.loc.line;
|
|
270
|
+
if (k < 0 || k >= r.lines)
|
|
271
|
+
continue;
|
|
272
|
+
const text = lines[r.start - 1 + k] ?? '';
|
|
273
|
+
const c = k === 0 ? col - r.loc.col + 1 : col;
|
|
274
|
+
if (c < 1 || c > text.length + 1)
|
|
275
|
+
continue;
|
|
276
|
+
return { line: r.start + k, col: c };
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
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, actions: p.actions, 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, actions: p.actions, 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,109 @@
|
|
|
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
|
+
/** an action (`archive`, `pgp.decrypt`): its arguments (`args`) or its awaited result */
|
|
52
|
+
| {
|
|
53
|
+
kind: 'call';
|
|
54
|
+
name: string;
|
|
55
|
+
args?: boolean;
|
|
56
|
+
};
|
|
57
|
+
export interface CheckExpr {
|
|
58
|
+
src: string;
|
|
59
|
+
/** `value` returns the expression; `handler` runs it as statements */
|
|
60
|
+
mode: 'value' | 'handler';
|
|
61
|
+
loc: CheckLoc;
|
|
62
|
+
/** frames visible here, in the order sinter resolves them last: states outer to inner, then loops outer to inner */
|
|
63
|
+
frames: number[];
|
|
64
|
+
/** tag of the element the expression sits on, for `$el` */
|
|
65
|
+
tag: string;
|
|
66
|
+
/** DOM event that runs a handler, for `$event` */
|
|
67
|
+
event: string | null;
|
|
68
|
+
/** `$result` (then) or `$error` (catch) is in scope */
|
|
69
|
+
result: boolean;
|
|
70
|
+
error: boolean;
|
|
71
|
+
/** type of `$result`, when the action matched an OpenAPI operation */
|
|
72
|
+
resultType?: TypeRef;
|
|
73
|
+
/** the value must be assignable to this (`data-sn-body`, `<sn-event>` `item` and `set`) */
|
|
74
|
+
returns?: TypeRef;
|
|
75
|
+
/** payload expressions (`<sn-event>`, `cursor`): the payload fields read, and the payload's type (null: any) */
|
|
76
|
+
payload?: {
|
|
77
|
+
names: string[];
|
|
78
|
+
type: TypeRef | null;
|
|
79
|
+
states?: string[];
|
|
80
|
+
};
|
|
81
|
+
/** the compiler reported an error in it (it may not even parse); kept for editor completion, never type-checked */
|
|
82
|
+
broken?: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface CheckResource {
|
|
85
|
+
name: string;
|
|
86
|
+
/** value of `type`, a TypeScript type; null when absent */
|
|
87
|
+
type: string | null;
|
|
88
|
+
/** start of the `type` value, or of the element when absent */
|
|
89
|
+
loc: CheckLoc;
|
|
90
|
+
/** start of the `name` value, where go to definition lands */
|
|
91
|
+
nameLoc?: CheckLoc;
|
|
92
|
+
/** without `type`: the OpenAPI operation `src` matched and the schema of what `items` selects */
|
|
93
|
+
openapi?: {
|
|
94
|
+
spec: number;
|
|
95
|
+
label: string;
|
|
96
|
+
schema: unknown;
|
|
97
|
+
};
|
|
98
|
+
/** `call`: the action and the `items` path into its result; with `type`, the result must fit it */
|
|
99
|
+
call?: {
|
|
100
|
+
name: string;
|
|
101
|
+
items: string | null;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export interface CheckUnit {
|
|
105
|
+
file: string;
|
|
106
|
+
resources: CheckResource[];
|
|
107
|
+
frames: CheckFrame[];
|
|
108
|
+
exprs: CheckExpr[];
|
|
109
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
/** the actions module (`data-sn-call`, `<sn-resource call>`), same shape as helpers */
|
|
29
|
+
actions?: HelperManifest;
|
|
30
|
+
}
|
|
31
|
+
export interface CompileResult {
|
|
32
|
+
html: string;
|
|
33
|
+
module: string;
|
|
34
|
+
diagnostics: Diagnostic[];
|
|
35
|
+
files: string[];
|
|
36
|
+
/** with `check: true` */
|
|
37
|
+
check?: CheckUnit;
|
|
38
|
+
}
|
|
39
|
+
export interface CompiledParts {
|
|
40
|
+
html: string;
|
|
41
|
+
ir: IR;
|
|
42
|
+
fns: string[];
|
|
43
|
+
features: string[];
|
|
44
|
+
/** helper names imported from the helpers module or the core */
|
|
45
|
+
helpers: {
|
|
46
|
+
user: string[];
|
|
47
|
+
builtin: string[];
|
|
48
|
+
};
|
|
49
|
+
namespaces: Record<string, string[]>;
|
|
50
|
+
/** action functions called, as written: `archive`, `pgp.decrypt` */
|
|
51
|
+
calls: string[];
|
|
52
|
+
diagnostics: Diagnostic[];
|
|
53
|
+
files: string[];
|
|
54
|
+
/** with `check: true` */
|
|
55
|
+
check?: CheckUnit;
|
|
56
|
+
}
|
|
57
|
+
export declare function compileParts(html: string, options: CompileOptions): CompiledParts;
|
|
58
|
+
export declare function normalize(p: string): string;
|
|
59
|
+
export declare function dirname(p: string): string;
|
|
60
|
+
export declare function basename(p: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* A row template's element lookup as straight-line code: each path prefix is walked once
|
|
63
|
+
* (`firstChild` / `nextSibling`), and an element whose tag does not match comes out null.
|
|
64
|
+
* The argument is the clone's first content node (the row element itself for a one-element row).
|
|
65
|
+
*/
|
|
66
|
+
export declare function findCode(nodes: NodeDef[]): string;
|