@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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -2
  3. package/dist/cache.d.ts +5 -0
  4. package/dist/cache.js +95 -0
  5. package/dist/check/index.d.ts +85 -0
  6. package/dist/check/index.js +156 -0
  7. package/dist/check/openapi-types.d.ts +6 -0
  8. package/dist/check/openapi-types.js +84 -0
  9. package/dist/check/shadow.d.ts +49 -0
  10. package/dist/check/shadow.js +279 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +67 -0
  13. package/dist/compiler/check-unit.d.ts +109 -0
  14. package/dist/compiler/check-unit.js +3 -0
  15. package/dist/compiler/compile.d.ts +66 -0
  16. package/dist/compiler/compile.js +1316 -0
  17. package/dist/compiler/diagnostic-docs.d.ts +10 -0
  18. package/dist/compiler/diagnostic-docs.js +142 -0
  19. package/dist/compiler/diagnostics.d.ts +57 -0
  20. package/dist/compiler/diagnostics.js +77 -0
  21. package/dist/compiler/expr.d.ts +97 -0
  22. package/dist/compiler/expr.js +853 -0
  23. package/dist/compiler/index.d.ts +22 -0
  24. package/dist/compiler/index.js +160 -0
  25. package/dist/compiler/openapi.d.ts +40 -0
  26. package/dist/compiler/openapi.js +177 -0
  27. package/dist/compiler/vocabulary.d.ts +12 -0
  28. package/dist/compiler/vocabulary.js +39 -0
  29. package/dist/dev.d.ts +10 -0
  30. package/dist/dev.js +92 -0
  31. package/dist/each.d.ts +3 -0
  32. package/dist/each.js +121 -0
  33. package/dist/index.d.ts +7 -0
  34. package/dist/index.js +4 -0
  35. package/dist/ir.d.ts +152 -0
  36. package/dist/ir.js +121 -0
  37. package/dist/lsp/quickfix.d.ts +15 -0
  38. package/dist/lsp/quickfix.js +58 -0
  39. package/dist/lsp/rpc.d.ts +31 -0
  40. package/dist/lsp/rpc.js +120 -0
  41. package/dist/lsp/server.d.ts +36 -0
  42. package/dist/lsp/server.js +281 -0
  43. package/dist/lsp/spec-location.d.ts +9 -0
  44. package/dist/lsp/spec-location.js +42 -0
  45. package/dist/lsp/typescript.d.ts +77 -0
  46. package/dist/lsp/typescript.js +242 -0
  47. package/dist/lsp/workspace.d.ts +56 -0
  48. package/dist/lsp/workspace.js +228 -0
  49. package/dist/optimistic.d.ts +3 -0
  50. package/dist/optimistic.js +15 -0
  51. package/dist/paginate.d.ts +3 -0
  52. package/dist/paginate.js +198 -0
  53. package/dist/persist.d.ts +3 -0
  54. package/dist/persist.js +55 -0
  55. package/dist/project.d.ts +42 -0
  56. package/dist/project.js +103 -0
  57. package/dist/runtime/app.d.ts +152 -0
  58. package/dist/runtime/app.js +1256 -0
  59. package/dist/runtime/config.d.ts +53 -0
  60. package/dist/runtime/config.js +118 -0
  61. package/dist/runtime/env.d.ts +18 -0
  62. package/dist/runtime/env.js +23 -0
  63. package/dist/runtime/helpers.d.ts +9 -0
  64. package/dist/runtime/helpers.js +55 -0
  65. package/dist/runtime/request.d.ts +23 -0
  66. package/dist/runtime/request.js +93 -0
  67. package/dist/runtime/security.d.ts +12 -0
  68. package/dist/runtime/security.js +34 -0
  69. package/dist/runtime/store.d.ts +98 -0
  70. package/dist/runtime/store.js +255 -0
  71. package/dist/sse.d.ts +7 -0
  72. package/dist/sse.js +209 -0
  73. package/dist/testing/index.d.ts +73 -0
  74. package/dist/testing/index.js +359 -0
  75. package/dist/vite.d.ts +11 -0
  76. package/dist/vite.js +96 -0
  77. package/dist/ws.d.ts +18 -0
  78. package/dist/ws.js +57 -0
  79. package/docs/diagnostics.md +283 -0
  80. package/html-custom-data.json +250 -0
  81. package/llms.txt +632 -0
  82. package/package.json +93 -4
package/dist/each.js ADDED
@@ -0,0 +1,121 @@
1
+ const isAbort = (e) => e instanceof DOMException && e.name == 'AbortError';
2
+ /**
3
+ * Sources are keyed by their resolved URL. An item belongs to a source when it came in that
4
+ * source's last response, or when `src` resolves to the source's URL with the item itself in
5
+ * place of the list item, so an item a server event added is replaced by the next response.
6
+ */
7
+ function fanOut(l, app) {
8
+ const { def, res: r } = l;
9
+ const each = def.each;
10
+ const sources = new Map();
11
+ const queued = new Set();
12
+ let pending = 0, again = null;
13
+ const urlOf = (item) => {
14
+ if (!item || typeof item != 'object')
15
+ return null;
16
+ try {
17
+ return app.url(def.src, Object.assign(Object.create(l.scope()), { [each.item]: item }));
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ };
23
+ /** the confirmed items minus those of source `u`, plus `items` */
24
+ const swap = (u, s, items) => {
25
+ const mine = (x) => s.keys.has(r.keyOf(x)) || urlOf(x) === u;
26
+ r.replace([...r.base().items.filter((x) => !mine(x)), ...items]);
27
+ };
28
+ async function load(u) {
29
+ const s = sources.get(u);
30
+ if (!s)
31
+ return;
32
+ s.c?.abort();
33
+ const ctl = (s.c = new AbortController()), n = ++s.seq, mark = r.startLoad();
34
+ let loaded = false;
35
+ pending++;
36
+ l.loading(true);
37
+ try {
38
+ const body = await l.get(u, ctl.signal);
39
+ if (n != s.seq || sources.get(u) != s)
40
+ return;
41
+ const items = l.items(body);
42
+ swap(u, s, items);
43
+ s.keys = new Set(items.map((x) => r.keyOf(x)));
44
+ s.answered = loaded = true;
45
+ r.meta.error = null;
46
+ // loadedAt: every current source has answered once
47
+ if ([...sources.values()].every((x) => x.answered))
48
+ r.meta.loadedAt = app.env.now();
49
+ }
50
+ catch (e) {
51
+ if (n != s.seq || isAbort(e))
52
+ return;
53
+ s.answered = true;
54
+ l.failed(e);
55
+ }
56
+ finally {
57
+ r.endLoad(mark, loaded);
58
+ if (n == s.seq)
59
+ s.c = null;
60
+ l.loading(--pending > 0);
61
+ }
62
+ }
63
+ l.changed = () => {
64
+ const scope = l.scope();
65
+ let list = [];
66
+ try {
67
+ if (def.when == null || app.fns[def.when](scope))
68
+ list = app.fns[each.list](scope) ?? [];
69
+ }
70
+ catch (e) {
71
+ app.warn(17, def.name, e.message);
72
+ }
73
+ const want = new Set();
74
+ for (const item of Array.isArray(list) ? list : [...list]) {
75
+ const u = urlOf(item);
76
+ if (u != null)
77
+ want.add(u);
78
+ }
79
+ for (const [u, s] of sources) {
80
+ if (want.has(u))
81
+ continue;
82
+ s.seq++;
83
+ s.c?.abort();
84
+ sources.delete(u);
85
+ swap(u, s, []);
86
+ }
87
+ for (const u of want) {
88
+ if (sources.has(u))
89
+ continue;
90
+ sources.set(u, { c: null, seq: 0, keys: new Set(), answered: false });
91
+ void load(u);
92
+ }
93
+ // nothing to load: loaded once the list itself is (an empty list is an answer, a pending one is not)
94
+ if (!sources.size && r.meta.loadedAt == null && (!each.of || app.resource(each.of).meta.loadedAt != null)) {
95
+ r.meta.loadedAt = app.env.now();
96
+ r.onDirty('meta');
97
+ }
98
+ l.loading(pending > 0);
99
+ };
100
+ l.start = () => l.changed();
101
+ l.load = () => Promise.all([...sources.keys()].map(load)).then(() => { });
102
+ // a hint (an event's payload) that resolves to one source's URL reloads only that source;
103
+ // several refetches in one task share one round of requests
104
+ l.refetch = (hint) => {
105
+ const u = hint ? urlOf(hint) : null;
106
+ queued.add(u != null && sources.has(u) ? u : '*');
107
+ return (again ??= Promise.resolve().then(() => {
108
+ again = null;
109
+ const urls = queued.has('*') ? [...sources.keys()] : [...queued];
110
+ queued.clear();
111
+ return Promise.all(urls.map(load)).then(() => { });
112
+ }));
113
+ };
114
+ }
115
+ const each = {
116
+ name: 'each',
117
+ install(app) {
118
+ app.ext.each = { fanOut: (loader) => fanOut(loader, app) };
119
+ },
120
+ };
121
+ export default each;
@@ -0,0 +1,7 @@
1
+ export { mount, instance, type App, type CallContext, type Feature, type MountOptions, type StateHandle, type Transport } from './runtime/app.js';
2
+ export { configure, credentials, header, onUnauthorized, loginRedirect, takeReturnUrl, clearPersisted, SinterError, type Config, type Middleware, type LoginRedirectOptions, } from './runtime/config.js';
3
+ export type { Resource, Change, Meta } from './runtime/store.js';
4
+ export { $k } from './runtime/security.js';
5
+ export { date, time, datetime, relative, number, bytes } from './runtime/helpers.js';
6
+ export type { Env } from './runtime/env.js';
7
+ export type { IR } from './ir.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { mount, instance } from './runtime/app.js';
2
+ export { configure, credentials, header, onUnauthorized, loginRedirect, takeReturnUrl, clearPersisted, SinterError, } from './runtime/config.js';
3
+ export { $k } from './runtime/security.js';
4
+ export { date, time, datetime, relative, number, bytes } from './runtime/helpers.js';
package/dist/ir.d.ts ADDED
@@ -0,0 +1,152 @@
1
+ export declare const IR_VERSION = 1;
2
+ /** Literal string parts and function indexes, joined with encodeURIComponent. */
3
+ export type UrlTemplate = Array<string | number>;
4
+ export interface Paginate {
5
+ mode: 'keyset' | 'offset' | 'append';
6
+ size: number;
7
+ sizeParam: string;
8
+ /** keyset/append: object literal evaluated with the boundary item as `$.$p` */
9
+ cursor: number | null;
10
+ directionParam: string;
11
+ /** keyset/append: the value sent for each direction, where it differs from the direction's name */
12
+ directionValues?: Partial<Record<'first' | 'prev' | 'next' | 'last', string>>;
13
+ pageParam: string;
14
+ /** keyset: offset parameter that lets `page(name, n)` jump; the server skips (n - 1) * size items */
15
+ offsetParam?: string;
16
+ }
17
+ export interface ResourceDef {
18
+ name: string;
19
+ src: UrlTemplate;
20
+ /** key fields; null for a single-object resource */
21
+ key: string[] | null;
22
+ items: string | null;
23
+ total: string | null;
24
+ /** field, `-field` for descending */
25
+ order: string | null;
26
+ when: number | null;
27
+ paginate: Paginate | null;
28
+ /** state and `$url` paths that `src` and `when` read */
29
+ deps: string[];
30
+ /** index of the node (in `roots`) whose scope src/when are evaluated in, -1 for page scope */
31
+ scope: number;
32
+ /** `data-sn` id of an embedded `<script type="application/json">`, or null */
33
+ data: number | null;
34
+ /** `cache`: the oldest cached data to show, in ms (0: any age); absent when not cached */
35
+ cache?: number;
36
+ /** `call="ns.fn(args)"`: loaded from an action function instead of `src` (then empty) */
37
+ call?: CallDef;
38
+ /** `each="a in accounts"`: one request per list item (src evaluated with `item` in scope), merged into one collection */
39
+ each?: {
40
+ item: string;
41
+ list: number;
42
+ of?: string;
43
+ };
44
+ }
45
+ export interface EventDef {
46
+ name: string;
47
+ resource: string;
48
+ op: 'upsert' | 'patch' | 'remove' | 'refetch';
49
+ item: number | null;
50
+ set: number | null;
51
+ when: number | null;
52
+ fallback: 'refetch' | null;
53
+ }
54
+ export interface SyncDef {
55
+ src: UrlTemplate;
56
+ events: EventDef[];
57
+ /** index of the node (in `roots`) whose scope `src` and the events' expressions are evaluated in, -1 (or absent) for page scope */
58
+ scope?: number;
59
+ }
60
+ export type BindingKind = 'text' | 'attr' | 'class' | 'style' | 'show' | 'model' | 'prop' | 'for' | 'if' | 'else' | 'on';
61
+ export interface Binding {
62
+ k: BindingKind;
63
+ /** value / handler function index */
64
+ f?: number;
65
+ /** dependency paths: dot-separated, `*` any segment, `{name}` scope value */
66
+ d?: string[];
67
+ /** event name (`on`), control kind (`model`) */
68
+ n?: string;
69
+ /** action index (`on`), template id (`for`, `if`, `else`) */
70
+ x?: number;
71
+ /** model setter function index */
72
+ a?: number;
73
+ /** if: node index of the paired else template */
74
+ y?: number;
75
+ /** handler: paths it may change */
76
+ w?: string[];
77
+ /** model: persist key */
78
+ p?: string;
79
+ /** the expression reads `$el` */
80
+ e?: 1;
81
+ /** the expression reads a loop index, so it re-runs when only the row's position changed */
82
+ i?: 1;
83
+ /** handler on an element without a method: `data-sn-debounce` milliseconds */
84
+ ms?: number;
85
+ }
86
+ export interface NodeDef {
87
+ /** `data-sn` id (roots) */
88
+ id?: number;
89
+ /** child indexes from the template content root (template nodes) */
90
+ path?: number[];
91
+ tag: string;
92
+ /** index of the nearest enclosing node in the same list that opens a state scope, -1 for none */
93
+ p: number;
94
+ /** state scope: initial value function and scope id (`$s<sid>`) */
95
+ state?: number;
96
+ sid?: number;
97
+ /** persist key for the whole state object */
98
+ persist?: string;
99
+ /** the element is a `<dialog>` or inside one of its block, so its bindings wait while that dialog is closed */
100
+ dl?: 1;
101
+ b: Binding[];
102
+ }
103
+ export interface TemplateDef {
104
+ id: number;
105
+ nodes: NodeDef[];
106
+ /** for: loop variable, index variable, internal index name */
107
+ item?: string;
108
+ index?: string | null;
109
+ ii?: string;
110
+ /** for: explicit key function, or resource key fields */
111
+ key?: number | null;
112
+ keyFields?: string[] | null;
113
+ /** for: function that finds the bound elements of one cloned row: (first content node) => elements, in `nodes` order */
114
+ find?: number;
115
+ }
116
+ /** a local action call: `args` evaluates the arguments (an array), `run(args, ctx)` calls the function */
117
+ export interface CallDef {
118
+ args: number;
119
+ run: number;
120
+ }
121
+ export interface ActionDef {
122
+ /** `CALL`: `data-sn-call`, with `call` set and an empty `url` */
123
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'CALL';
124
+ url: UrlTemplate;
125
+ call?: CallDef;
126
+ body: number | null;
127
+ trigger: string;
128
+ debounce: number;
129
+ concurrency: 'block' | 'queue' | 'latest';
130
+ optimistic: number | null;
131
+ then: number | null;
132
+ catch: number | null;
133
+ /** paths `then` and `catch` may change */
134
+ w: string[];
135
+ /** element is a form (serialize, disable in block mode) */
136
+ form: boolean;
137
+ }
138
+ export interface IR {
139
+ v: 1;
140
+ resources: ResourceDef[];
141
+ syncs: SyncDef[];
142
+ roots: NodeDef[];
143
+ templates: TemplateDef[];
144
+ actions: ActionDef[];
145
+ /** page uses `$now` */
146
+ now?: boolean;
147
+ /** `data-sn-path` patterns; `:name` segments of the first match join `$url` */
148
+ path?: string[];
149
+ }
150
+ export type Fn = (scope: any, value?: any) => any;
151
+ /** Structural check of an IR against v1; returns problems (empty when valid). */
152
+ export declare function validateIR(ir: unknown, fnCount: number): string[];
package/dist/ir.js ADDED
@@ -0,0 +1,121 @@
1
+ // IR v1: the only contract between compiler and runtime. Plain JSON; every
2
+ // expression is an index into the page module's function table.
3
+ export const IR_VERSION = 1;
4
+ /** Structural check of an IR against v1; returns problems (empty when valid). */
5
+ export function validateIR(ir, fnCount) {
6
+ const errs = [];
7
+ const o = ir;
8
+ const fn = (v, at, nullable = true) => {
9
+ if (v === null && nullable)
10
+ return;
11
+ if (v === undefined && nullable)
12
+ return;
13
+ if (!Number.isInteger(v) || v < 0 || v >= fnCount)
14
+ errs.push(`${at}: bad function index ${String(v)}`);
15
+ };
16
+ const url = (u, at) => {
17
+ if (!Array.isArray(u) || !u.length || typeof u[0] !== 'string') {
18
+ errs.push(`${at}: URL template must start with a literal`);
19
+ return;
20
+ }
21
+ u.forEach((p, i) => (typeof p === 'string' ? undefined : fn(p, `${at}[${i}]`, false)));
22
+ };
23
+ if (!o || o.v !== 1)
24
+ return ['v must be 1'];
25
+ for (const k of ['resources', 'syncs', 'roots', 'templates', 'actions'])
26
+ if (!Array.isArray(o[k]))
27
+ errs.push(`${k} must be an array`);
28
+ if (errs.length)
29
+ return errs;
30
+ const names = new Set();
31
+ o.resources.forEach((r, i) => {
32
+ const at = `resources[${i}]`;
33
+ if (!/^[A-Za-z_]\w*$/.test(r.name))
34
+ errs.push(`${at}: bad name`);
35
+ if (names.has(r.name))
36
+ errs.push(`${at}: duplicate name ${r.name}`);
37
+ names.add(r.name);
38
+ url(r.src, `${at}.src`);
39
+ fn(r.when, `${at}.when`);
40
+ if (r.paginate) {
41
+ if (!['keyset', 'offset', 'append'].includes(r.paginate.mode))
42
+ errs.push(`${at}: bad paginate mode`);
43
+ fn(r.paginate.cursor, `${at}.paginate.cursor`);
44
+ }
45
+ });
46
+ o.syncs.forEach((s, i) => {
47
+ url(s.src, `syncs[${i}].src`);
48
+ s.events.forEach((e, j) => {
49
+ const at = `syncs[${i}].events[${j}]`;
50
+ if (!names.has(e.resource))
51
+ errs.push(`${at}: unknown resource ${e.resource}`);
52
+ if (!['upsert', 'patch', 'remove', 'refetch'].includes(e.op))
53
+ errs.push(`${at}: bad op`);
54
+ fn(e.item, `${at}.item`);
55
+ fn(e.set, `${at}.set`);
56
+ fn(e.when, `${at}.when`);
57
+ });
58
+ });
59
+ const kinds = new Set(['text', 'attr', 'class', 'style', 'show', 'model', 'prop', 'for', 'if', 'else', 'on']);
60
+ const nodes = (list, at, inTemplate) => list.forEach((n, i) => {
61
+ const a = `${at}[${i}]`;
62
+ if (inTemplate ? !Array.isArray(n.path) : !Number.isInteger(n.id))
63
+ errs.push(`${a}: needs ${inTemplate ? 'path' : 'id'}`);
64
+ if (n.p >= i)
65
+ errs.push(`${a}: parent scope must come first`);
66
+ if (n.state !== undefined) {
67
+ fn(n.state, `${a}.state`, false);
68
+ if (!Number.isInteger(n.sid))
69
+ errs.push(`${a}: state needs sid`);
70
+ }
71
+ n.b.forEach((b, j) => {
72
+ const ba = `${a}.b[${j}]`;
73
+ if (!kinds.has(b.k))
74
+ errs.push(`${ba}: bad kind ${b.k}`);
75
+ if (b.k === 'on') {
76
+ if (!b.n)
77
+ errs.push(`${ba}: on needs an event name`);
78
+ if (b.x !== undefined) {
79
+ if (!o.actions[b.x])
80
+ errs.push(`${ba}: bad action ${b.x}`);
81
+ }
82
+ else
83
+ fn(b.f, `${ba}.f`, false);
84
+ }
85
+ else if (b.k === 'for' || b.k === 'if' || b.k === 'else') {
86
+ if (!o.templates[b.x])
87
+ errs.push(`${ba}: bad template ${b.x}`);
88
+ if (b.k !== 'else')
89
+ fn(b.f, `${ba}.f`, false);
90
+ if (b.y !== undefined && !list[b.y]?.b.some((x) => x.k === 'else'))
91
+ errs.push(`${ba}: y must point at an else node`);
92
+ }
93
+ else
94
+ fn(b.f, `${ba}.f`, false);
95
+ if (b.k === 'model')
96
+ fn(b.a, `${ba}.a`, false);
97
+ for (const d of [...(b.d ?? []), ...(b.w ?? [])])
98
+ if (typeof d !== 'string' || !d)
99
+ errs.push(`${ba}: bad path`);
100
+ });
101
+ });
102
+ nodes(o.roots, 'roots', false);
103
+ o.templates.forEach((t, i) => {
104
+ if (t.id !== i)
105
+ errs.push(`templates[${i}]: id must equal its index`);
106
+ nodes(t.nodes, `templates[${i}].nodes`, true);
107
+ fn(t.key, `templates[${i}].key`);
108
+ fn(t.find, `templates[${i}].find`);
109
+ });
110
+ o.actions.forEach((a, i) => {
111
+ const at = `actions[${i}]`;
112
+ if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(a.method))
113
+ errs.push(`${at}: bad method`);
114
+ if (!['block', 'queue', 'latest'].includes(a.concurrency))
115
+ errs.push(`${at}: bad concurrency`);
116
+ url(a.url, `${at}.url`);
117
+ for (const k of ['body', 'optimistic', 'then', 'catch'])
118
+ fn(a[k], `${at}.${k}`);
119
+ });
120
+ return errs;
121
+ }
@@ -0,0 +1,15 @@
1
+ import type { Range } from './server.js';
2
+ export interface TextEdit {
3
+ range: Range;
4
+ newText: string;
5
+ }
6
+ /** The suggestion and the misspelled name it replaces, from a diagnostic's message. */
7
+ export declare function suggestionOf(message: string): {
8
+ wrong: string;
9
+ right: string;
10
+ } | null;
11
+ /**
12
+ * The edit for a diagnostic: the occurrence of the misspelled name on the diagnostic's
13
+ * lines that is nearest its start, replaced by the suggestion.
14
+ */
15
+ export declare function didYouMeanEdit(text: string, range: Range, message: string): TextEdit | null;
@@ -0,0 +1,58 @@
1
+ function distance(a, b) {
2
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
3
+ for (let i = 1; i <= a.length; i++) {
4
+ let diag = prev[0];
5
+ prev[0] = i;
6
+ for (let j = 1; j <= b.length; j++) {
7
+ const tmp = prev[j];
8
+ prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
9
+ diag = tmp;
10
+ }
11
+ }
12
+ return prev[b.length];
13
+ }
14
+ /** The suggestion and the misspelled name it replaces, from a diagnostic's message. */
15
+ export function suggestionOf(message) {
16
+ const m = /Did you mean [`']([^`']+)[`']\?/.exec(message);
17
+ if (!m)
18
+ return null;
19
+ const right = m[1];
20
+ const before = message.slice(0, m.index);
21
+ let best = null;
22
+ for (const q of before.matchAll(/`([^`]+)`|'([^']+)'/g)) {
23
+ // `m.inbox_titel`, `$meta.contacts.loadng`: the suggestion replaces the last part
24
+ const whole = q[1] ?? q[2];
25
+ const wrong = /^[\w$-]+$/.test(right) ? whole.split(/[.\s/]/).pop() : whole;
26
+ if (!wrong || wrong === right)
27
+ continue;
28
+ const d = distance(wrong, right);
29
+ if (!best || d < best.d)
30
+ best = { wrong, d };
31
+ }
32
+ return best ? { wrong: best.wrong, right } : null;
33
+ }
34
+ /**
35
+ * The edit for a diagnostic: the occurrence of the misspelled name on the diagnostic's
36
+ * lines that is nearest its start, replaced by the suggestion.
37
+ */
38
+ export function didYouMeanEdit(text, range, message) {
39
+ const s = suggestionOf(message);
40
+ if (!s)
41
+ return null;
42
+ const lines = text.split('\n');
43
+ let best = null;
44
+ for (let l = range.start.line; l <= range.end.line && l < lines.length; l++) {
45
+ const t = lines[l];
46
+ for (let at = t.indexOf(s.wrong); at >= 0; at = t.indexOf(s.wrong, at + 1)) {
47
+ // a whole name, not part of a longer one
48
+ if (/[\w$]/.test(t[at - 1] ?? '') || /[\w$]/.test(t[at + s.wrong.length] ?? ''))
49
+ continue;
50
+ const d = l === range.start.line ? Math.abs(at - range.start.character) : 1e6 + at;
51
+ if (!best || d < best.d)
52
+ best = { line: l, at, d };
53
+ }
54
+ }
55
+ if (!best)
56
+ return null;
57
+ return { range: { start: { line: best.line, character: best.at }, end: { line: best.line, character: best.at + s.wrong.length } }, newText: s.right };
58
+ }
@@ -0,0 +1,31 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ type Handler = (params: any) => unknown;
3
+ export declare class RpcError extends Error {
4
+ readonly code: number;
5
+ constructor(code: number, message: string);
6
+ }
7
+ export declare class Connection {
8
+ private output;
9
+ private buf;
10
+ private nextId;
11
+ private pending;
12
+ private requests;
13
+ private notifications;
14
+ private closed;
15
+ /** called for a notification nothing handles */
16
+ onUnhandled?: (method: string, params: unknown) => void;
17
+ onClose?: () => void;
18
+ /** answers a request nothing handles (default: a method-not-found error) */
19
+ requestFallback?: (method: string, params: unknown) => unknown;
20
+ constructor(input: Readable, output: Writable);
21
+ onRequest(method: string, handler: Handler): void;
22
+ onNotification(method: string, handler: Handler): void;
23
+ request<T = any>(method: string, params?: unknown): Promise<T>;
24
+ notify(method: string, params?: unknown): void;
25
+ private send;
26
+ private receive;
27
+ private dispatch;
28
+ private log;
29
+ close(): void;
30
+ }
31
+ export {};
@@ -0,0 +1,120 @@
1
+ export class RpcError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ }
7
+ }
8
+ export class Connection {
9
+ output;
10
+ buf = Buffer.alloc(0);
11
+ nextId = 1;
12
+ pending = new Map();
13
+ requests = new Map();
14
+ notifications = new Map();
15
+ closed = false;
16
+ /** called for a notification nothing handles */
17
+ onUnhandled;
18
+ onClose;
19
+ /** answers a request nothing handles (default: a method-not-found error) */
20
+ requestFallback;
21
+ constructor(input, output) {
22
+ this.output = output;
23
+ input.on('data', (chunk) => this.receive(chunk));
24
+ input.on('end', () => this.close());
25
+ input.on('close', () => this.close());
26
+ }
27
+ onRequest(method, handler) { this.requests.set(method, handler); }
28
+ onNotification(method, handler) { this.notifications.set(method, handler); }
29
+ request(method, params) {
30
+ if (this.closed)
31
+ return Promise.reject(new Error(`connection closed (${method})`));
32
+ const id = this.nextId++;
33
+ return new Promise((resolve, reject) => {
34
+ this.pending.set(id, { resolve, reject });
35
+ this.send({ id, method, params });
36
+ });
37
+ }
38
+ notify(method, params) {
39
+ if (!this.closed)
40
+ this.send({ method, params });
41
+ }
42
+ send(message) {
43
+ const body = JSON.stringify({ jsonrpc: '2.0', ...message });
44
+ this.output.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
45
+ }
46
+ receive(chunk) {
47
+ this.buf = this.buf.length ? Buffer.concat([this.buf, chunk]) : chunk;
48
+ for (;;) {
49
+ const end = this.buf.indexOf('\r\n\r\n');
50
+ if (end < 0)
51
+ return;
52
+ const m = /Content-Length:\s*(\d+)/i.exec(this.buf.subarray(0, end).toString('ascii'));
53
+ if (!m) {
54
+ this.buf = this.buf.subarray(end + 4);
55
+ continue;
56
+ }
57
+ const len = Number(m[1]);
58
+ if (this.buf.length < end + 4 + len)
59
+ return;
60
+ const body = this.buf.subarray(end + 4, end + 4 + len).toString('utf8');
61
+ this.buf = this.buf.subarray(end + 4 + len);
62
+ let msg;
63
+ try {
64
+ msg = JSON.parse(body);
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ this.dispatch(msg);
70
+ }
71
+ }
72
+ dispatch(msg) {
73
+ if (msg.method === undefined) {
74
+ const p = this.pending.get(msg.id);
75
+ if (!p)
76
+ return;
77
+ this.pending.delete(msg.id);
78
+ if (msg.error)
79
+ p.reject(new RpcError(msg.error.code, msg.error.message));
80
+ else
81
+ p.resolve(msg.result ?? null);
82
+ return;
83
+ }
84
+ if (msg.id === undefined) {
85
+ const h = this.notifications.get(msg.method);
86
+ if (h) {
87
+ try {
88
+ void Promise.resolve(h(msg.params)).catch((e) => this.log(msg.method, e));
89
+ }
90
+ catch (e) {
91
+ this.log(msg.method, e);
92
+ }
93
+ }
94
+ else
95
+ this.onUnhandled?.(msg.method, msg.params);
96
+ return;
97
+ }
98
+ const fallback = this.requestFallback;
99
+ const h = this.requests.get(msg.method) ?? (fallback && ((params) => fallback(msg.method, params)));
100
+ if (!h) {
101
+ this.send({ id: msg.id, error: { code: -32601, message: `unhandled method ${msg.method}` } });
102
+ return;
103
+ }
104
+ Promise.resolve()
105
+ .then(() => h(msg.params))
106
+ .then((result) => this.send({ id: msg.id, result: result ?? null }), (e) => this.send({ id: msg.id, error: { code: e instanceof RpcError ? e.code : -32603, message: e instanceof Error ? e.message : String(e) } }));
107
+ }
108
+ log(method, e) {
109
+ process.stderr.write(`sinter lsp: ${method}: ${e instanceof Error ? e.stack ?? e.message : String(e)}\n`);
110
+ }
111
+ close() {
112
+ if (this.closed)
113
+ return;
114
+ this.closed = true;
115
+ for (const p of this.pending.values())
116
+ p.reject(new Error('connection closed'));
117
+ this.pending.clear();
118
+ this.onClose?.();
119
+ }
120
+ }
@@ -0,0 +1,36 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ import { type Diagnostic } from '../compiler/index.js';
3
+ import { Connection } from './rpc.js';
4
+ /** How long typing must pause before TypeScript checks the pages. */
5
+ export declare const TYPECHECK_DELAY = 300;
6
+ export interface Position {
7
+ line: number;
8
+ character: number;
9
+ }
10
+ export interface Range {
11
+ start: Position;
12
+ end: Position;
13
+ }
14
+ export interface LspDiagnostic {
15
+ range: Range;
16
+ severity: 1 | 2;
17
+ code: string;
18
+ codeDescription: {
19
+ href: string;
20
+ };
21
+ source: 'sinter';
22
+ message: string;
23
+ }
24
+ /** The range a diagnostic underlines: the name or word at its position, else one character. */
25
+ export declare function rangeOf(text: string, line: number, col: number): Range;
26
+ export declare function toLsp(d: Diagnostic, text: string): LspDiagnostic;
27
+ export interface ServerOptions {
28
+ exit?: (code: number) => void;
29
+ /** milliseconds between the last change and the type check (default TYPECHECK_DELAY) */
30
+ typecheckDelay?: number;
31
+ /** TypeScript's `tsc` script for every project (default: resolved from each project) */
32
+ tsc?: string;
33
+ }
34
+ export declare function startServer(input: Readable, output: Writable, o?: ServerOptions): Connection;
35
+ /** The `src` of an `<sn-include>` when the 0-based position is inside its value, else null. */
36
+ export declare function includeAt(text: string, line: number, character: number): string | null;